rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -0,0 +1,90 @@
1
+ # 03 — Target: the project's own code
2
+
3
+ A gem's shipped `sig/`, an application's `sig/`, or rbs-inline annotations (`#:`, `# @rbs`) in either.
4
+ The project's owner is the authority on every contract this code makes, so this target is
5
+ **interactive**: Rigor and the documentation narrow the questions, and the owner answers the ones they
6
+ leave open.
7
+
8
+ ## Setup
9
+
10
+ The project's Rigor configuration already analyses its source with its `sig/` on the signature path, so
11
+ the commands in [`01-finding-candidates.md`](01-finding-candidates.md) run as they are:
12
+
13
+ ```sh
14
+ rigor check --no-baseline --format=json lib # Signal 1
15
+ rigor sig-gen --diff --format=json lib # Signal 2
16
+ rigor check --no-baseline --format=json spec # Signal 3, or test/, or the application's callers
17
+ ```
18
+
19
+ If `rigor` is not configured yet, run `rigor-next-steps` first. Record the current `rigor check` result
20
+ (with the baseline, as the project normally runs it) before changing anything; the gate below compares
21
+ against it.
22
+
23
+ ## Ask, in one batch
24
+
25
+ After triage, sort what is left into three groups:
26
+
27
+ 1. **Settled by evidence** — a `def.return-type-mismatch`, a `sig-gen` proposal over `untyped`, a
28
+ documented value. Apply these, with their citations.
29
+ 2. **Settled as "leave it"** — a `void` over a value nobody documented, a wide nominal, a proposal that
30
+ is an inference gap. Do not ask about these one by one; list them.
31
+ 3. **Open contract questions** — everything that needs the owner. Ask them together, each with the
32
+ evidence and the concrete options:
33
+
34
+ > `Client#push` is declared `-> String`. Its body returns `nil` when a middleware halts the job
35
+ > (`lib/client.rb:88`, `return nil unless item`), and `rigor check` cannot see that early return.
36
+ > Should callers be told to expect `nil`?
37
+ > (a) `-> String?` — callers must handle a halted job; (b) keep `-> String` and raise instead of
38
+ > returning `nil`; (c) `-> void` — callers should not use the value.
39
+
40
+ Never ask an open question as "what type is this?". The owner should choose a contract, not reverse-
41
+ engineer one.
42
+
43
+ ## Write the answer where it can be cited
44
+
45
+ An answer that lives only in the conversation is lost to the next contributor and to anyone writing RBS
46
+ for this library elsewhere. Offer to record each answered contract as a doc comment in prose — "Returns
47
+ the job id, or nil when a middleware stops the job." — next to the method. The declaration then has a
48
+ documented source, which is what makes it checkable later.
49
+
50
+ If the project keeps types out of doc comments, prose describing behaviour is still fine; a bracketed
51
+ YARD type is not, and `rigor-type-oracle` covers why.
52
+
53
+ ## The gate
54
+
55
+ 1. `rigor check` on the project, run as the project normally runs it, gains no new diagnostic. A new one
56
+ means a caller relied on the old contract: show it to the owner. It is either a bug the new contract
57
+ exposed, or a sign the contract change is wrong.
58
+ 2. If the project ships its RBS (a gem's `sig/`), it still loads: `rbs -I sig validate`, or the
59
+ project's own RBS task.
60
+ 3. If the project also runs Steep or another RBS consumer, run it. Rigor is not the only reader of these
61
+ files.
62
+ 4. Where you widened a return, a witness shows the case is real (see `01-finding-candidates.md`
63
+ § "Signal 6") and a test or a use site now handles it; where you widened a parameter, a test now
64
+ passes the newly accepted argument. The owner may accept a change without a witness — say so in the
65
+ write-up — but a Rigor finding alone is never presented as proof.
66
+ 5. If the project has a test suite, use it as the command for the declaration-mutation pass
67
+ ([`05-declaration-mutants.md`](05-declaration-mutants.md)): it runs the suite under `rbs/test/setup`
68
+ once per broken declaration. A killed mutant is a declaration the suite checks against real values,
69
+ which no static signal does; a survivor is one it does not check, however green the suite is. Report
70
+ the survivors to the owner.
71
+ 6. For a gem with no RBS yet, the skeleton and new-method rules in `04-target-gem-rbs-collection.md`
72
+ § "New declaration set" apply to its `sig/` as well; ship it with the gem rather than through the
73
+ collection.
74
+
75
+ ## When the wrong signature belongs to a dependency
76
+
77
+ If a diagnostic in the project traces to a dependency's community RBS, the fix belongs upstream:
78
+ follow [`04-target-gem-rbs-collection.md`](04-target-gem-rbs-collection.md) for that gem. Until the fix
79
+ is released, a local corrected copy can stand in:
80
+
81
+ - in `rbs_collection.yaml`, set that gem to `ignore: true`, then run `rbs collection install` again so
82
+ `rbs_collection.lock.yaml` drops it (the old directory under `.gem_rbs_collection/` stays, but only
83
+ gems listed in the lockfile load) — otherwise the collection copy still loads next to yours and the duplicate declarations fail to build, which reads as a quieter run, not a
84
+ fixed one;
85
+ - copy its RBS into a project directory on `signature_paths`, apply the fix, and note the upstream pull
86
+ request next to it;
87
+ - remove the copy and the `ignore:` once the fix is in a collection release you have installed.
88
+
89
+ Do not patch the installed collection under `.gem_rbs_collection/`; the next `rbs collection install`
90
+ overwrites it.
@@ -0,0 +1,210 @@
1
+ # 04 — Target: a gem_rbs_collection entry
2
+
3
+ RBS for a gem you do not own, in a checkout of
4
+ [`ruby/gem_rbs_collection`](https://github.com/ruby/gem_rbs_collection). The gem's documentation is the
5
+ authority, so this target can run without questions up to a finished branch and pull request
6
+ description. **Pushing the branch and opening the pull request are the user's call**: both are
7
+ outward-facing, even to a fork. Show them the local branch and the description, and push or open only
8
+ on their go-ahead, unless they already gave it for this change.
9
+
10
+ The collection's own guide is `docs/CONTRIBUTING.md` in the checkout; read it once. Two of its points
11
+ shape this workflow: focus on the API the README and documentation present, and write `_test/` code that
12
+ checks RBS against *application* code, not against the gem's implementation.
13
+
14
+ ## Setup
15
+
16
+ Paths below use `gems/NAME/X.Y` for the entry, `$COLLECTION` for the checkout, `$GEM_DIR` for the gem's
17
+ source and `$SCRATCH` for a scratch directory. **Run every block from here to Signal 3 in one shell
18
+ session**, or as one script: an agent's shell tool often keeps no variables between calls, and an empty
19
+ `$GEM_DIR` or `$SCRATCH` turns `cp -R "$GEM_DIR/lib" "$SCRATCH/lib"` into a copy of `/lib`. If the
20
+ variables cannot persist, set them again at the top of each block and guard them, for example
21
+ `: "${GEM_DIR:?}" "${SCRATCH:?}"`.
22
+
23
+ 1. In the checkout: `bin/setup`.
24
+ 2. Fetch the source of the version the entry describes. Unpacking reads it without building native
25
+ extensions or running anything:
26
+
27
+ ```sh
28
+ COLLECTION=$PWD
29
+ SRC=$(mktemp -d)
30
+ (cd "$SRC" && gem fetch NAME -v '~> X.Y.0' && gem unpack NAME-*.gem)
31
+ GEM_DIR=$(ls -d "$SRC"/NAME-*/ | head -n 1)
32
+ ```
33
+
34
+ If the gem is already installed, `ruby -e 'puts Gem::Specification.find_by_name("NAME", "~> X.Y.0").gem_dir'`
35
+ gives the same directory.
36
+
37
+ 3. Run the entry's tests once before changing anything, so you know they were green:
38
+
39
+ ```sh
40
+ bin/test gems/NAME/X.Y
41
+ ```
42
+
43
+ This also leaves an `rbs_collection.lock.yaml` in `_test/`, which puts the entry's dependency RBS in
44
+ reach for the use-site check below.
45
+
46
+ ## Signals 1 and 2 — the declarations against the gem's source
47
+
48
+ Work in a scratch directory outside the checkout, so nothing Rigor writes ends up in the diff, and copy
49
+ in only what is under review:
50
+
51
+ ```sh
52
+ SCRATCH=$(mktemp -d)
53
+ mkdir -p "$SCRATCH/sig"
54
+ cp -R "$GEM_DIR/lib" "$SCRATCH/lib"
55
+ rsync -a --exclude _test --exclude .gem_rbs_collection "$COLLECTION/gems/NAME/X.Y/" "$SCRATCH/sig/"
56
+ printf 'paths: ["lib"]\nsignature_paths: ["sig"]\n' > "$SCRATCH/.rigor.yml"
57
+ cd "$SCRATCH"
58
+ rigor check --format=json lib
59
+ rigor sig-gen --diff --format=json lib
60
+ ```
61
+
62
+ Excluding `_test/` matters: its `.rbs` files and any `.gem_rbs_collection/` left by an earlier
63
+ `bin/test` would load as duplicate declarations. A gem whose RBS depends on another gem's RBS will show
64
+ `rbs.coverage.*` warnings and undefined-method noise here; copy that dependency's entry into `sig/` too,
65
+ or read Signal 1 for the methods whose types do not touch the dependency.
66
+
67
+ ## Signal 3 — the entry's `_test/` code
68
+
69
+ ```sh
70
+ cd "$COLLECTION/gems/NAME/X.Y/_test"
71
+ printf 'paths: ["."]\nsignature_paths: ["."]\n' > .rigor.yml # with rbs_collection.lock.yaml present
72
+ rigor check --no-cache --format=json
73
+ ```
74
+
75
+ Without an `rbs_collection.lock.yaml`, use `signature_paths: [".."]` instead, and expect missing
76
+ dependency RBS. `--no-cache` keeps a `.rigor/` cache directory out of the entry. Keep `.rigor.yml` out of
77
+ the commit by listing it in `.git/info/exclude`, and check `git status` before committing: nothing under
78
+ `_test/` but your test files belongs in the diff.
79
+
80
+ ## Signal 5 — the gem's documentation
81
+
82
+ Read the doc comments in `$GEM_DIR/lib` and the README in `$GEM_DIR` — the source of the version under
83
+ review. Quote what you rely on in the write-up with its path and line in that source. A website or the
84
+ default branch's README may describe another release: use it to ask, not to decide.
85
+
86
+ ## Signal 6 — the runtime witness
87
+
88
+ Every change that makes a declaration say less needs a witness or that version's documentation here; a
89
+ Rigor finding alone does not go upstream. Give both when you can. Run the witness from Signal 6 of
90
+ `01-finding-candidates.md` in `$SCRATCH`, against the unpacked source and the copied declarations:
91
+
92
+ ```sh
93
+ : "${SCRATCH:?}"
94
+ cd "$SCRATCH"
95
+ RBS_TEST_TARGET='Namespace::*' RBS_TEST_OPT='-I sig' ruby -I lib -rrbs/test/setup witness.rb
96
+ ```
97
+
98
+ **This executes the gem's code with the user's privileges, and installing what it needs at runtime
99
+ installs more.** A scratch directory is not isolation. Run it in a container or VM, or ask the user
100
+ before executing a gem that is not already in their bundle. Record the call and the error. After editing the declaration,
101
+ copy it into `$SCRATCH/sig` again and confirm the same run passes. `witness.rb` is evidence for the pull
102
+ request description, not a committed test: the collection's `_test/` code is checked statically.
103
+
104
+ ## Writing the change
105
+
106
+ - Edit `gems/NAME/X.Y/*.rbs`. Keep types inside the gem's namespace; no new top-level aliases or
107
+ interfaces.
108
+ - A new standard-library or gem dependency in the RBS, or in the test, goes into `manifest.yaml`
109
+ (marcel's test needed `tempfile`).
110
+ - Apply the same contract to every version directory whose code and documentation agree, and only to
111
+ those.
112
+
113
+ ## New declaration set (a new entry)
114
+
115
+ When the gem has no entry yet, there is nothing for Signals 1 and 2 to compare against, and three jobs
116
+ take the place of the audit:
117
+
118
+ 1. **The skeleton.** `"$COLLECTION"/bin/init_new_gem NAME` creates the directory and `_test/`. It
119
+ prompts on stdin for the version and then a GitHub account for `_reviewers.yaml`, so answer both
120
+ explicitly — `printf '0.6\n\n' | "$COLLECTION"/bin/init_new_gem NAME` — and leave the account empty
121
+ unless the user gives you one; never put their name in it on your own. Take the
122
+ module structure — classes, modules, `include` / `extend`, superclasses, `attr_*`, aliases,
123
+ constants, and methods that exist only through metaprogramming — from the loaded gem, because
124
+ `sig-gen` writes none of these:
125
+
126
+ ```sh
127
+ RUBYLIB="$GEM_DIR/lib" rbs prototype runtime -r FEATURE Namespace 'Namespace::*' > "$SCRATCH/skeleton.rbs"
128
+ ```
129
+
130
+ `-r` takes the gem's feature name (`rgl/adjacency`); `-R` means `require_relative`, and there is no
131
+ `-I`. Name the top-level module on its own as well as `Namespace::*`: the pattern alone excludes it. rbs 4.2 marks the command deprecated (ruby/rbs#2841) but it still works. It loads the gem, so
132
+ the witness safety rule applies.
133
+ 2. **The types.** Run `rigor sig-gen --print` in `$SCRATCH` with an empty `sig/`, and take each
134
+ method's types from it under `02-contract-rules.md`: a literal, empty-collection, `nil` or
135
+ procedure-tail proposal becomes `void` or `bool` unless documented; blocks follow § "Blocks".
136
+ 3. **The contracts.** Fill what `sig-gen` skipped from the documentation, witness every `nil`, and leave
137
+ the rest `untyped`. A method that appears in the skeleton but in neither `sig-gen` list was silently
138
+ dropped (#3068); give it a disposition too.
139
+
140
+ **A dependency without an entry.** When the new entry must reference a type from another gem — a mixin
141
+ (`include Stream`), a superclass, a parameter type — and that gem has no entry either, the other entry
142
+ comes first, in its own pull request, or the reference is dropped (with the methods it would have
143
+ supplied declared directly). Say which in the description. Never ship a stub of another gem inside this
144
+ entry.
145
+
146
+ ## The gate
147
+
148
+ 1. Add or extend a test under `_test/` that exercises the contract you changed, as application code
149
+ would: `&.` or an `if` on a newly nilable return, calls to newly declared methods, the newly accepted
150
+ argument shapes. A new file pair `test_N.rb` / `test_N.rbs` is fine when the existing test does not
151
+ fit.
152
+ 2. `"$COLLECTION"/bin/test gems/NAME/X.Y`, run from `$COLLECTION`, passes for every version you
153
+ touched. It runs `rbs validate`, `steep check` and the untyped-call check.
154
+ 3. `rigor check` on `_test/` reports nothing new compared with before the change.
155
+ 4. Every change that says less has a witness that fails under the old declaration and passes under the
156
+ new one, or a quote from that version's documentation, for each version you changed.
157
+ 5. The declaration-mutation pass ([`05-declaration-mutants.md`](05-declaration-mutants.md)) has run over
158
+ the entry with `_test/` and your witnesses as the command, and its survivors are listed in the
159
+ description.
160
+
161
+ When `bin/test` cannot run — it needs the network to lock and install the collection — run the same
162
+ three checks offline and say in the description that `bin/test` was not run. Offline there is no
163
+ installed collection, so the Steepfile names the entry (`signature ".."`) and each dependency entry
164
+ itself:
165
+
166
+ Add `-r <library>` to `rbs` and a `library "<library>"` line to the Steepfile for each standard library
167
+ `manifest.yaml` lists, which `bin/test` would otherwise install. A library that is part of core in the
168
+ `rbs` you run (`set` under rbs 4.2) has no separate signatures, and `-r set` fails with "Cannot find
169
+ type definitions"; leave it out there:
170
+
171
+ ```sh
172
+ rbs -r <each manifest library> -I "$COLLECTION/gems/NAME/X.Y" -I <each dependency entry> validate
173
+ cd "$COLLECTION/gems/NAME/X.Y/_test" # Steepfile: bin/test's generate_steepfile, plus signature ".." and each dependency entry
174
+ steep check
175
+ steep stats --format=csv # every row's "Untyped calls" must be 0, as bin/check-untyped-call.rb checks
176
+ ```
177
+
178
+ ## The pull request
179
+
180
+ Branch from the collection's default branch and keep one gem, one concern per pull request. Title it
181
+ `NAME: <what the contract now says>` — for example `sidekiq: Client#push and client_push return
182
+ String?`. The body follows the shape reviewers have accepted:
183
+
184
+ ```markdown
185
+ ## Summary
186
+
187
+ What was wrong with the contract and why, with the quote that settles it.
188
+
189
+ Changed in `gems/NAME/X.Y`:
190
+
191
+ - `Klass#meth` now returns `String?`. <the evidence, one line>
192
+
193
+ Deliberately left unchanged:
194
+
195
+ - `Klass#other` stays `void`. Its implementation returns the same value, but NAME does not document a return value for it.
196
+ - `Klass#memo` stays `Chain`. Rigor reports `Chain?`, but no input returns `nil` (the value is memoised).
197
+
198
+ ## How this was found
199
+
200
+ The signal that surfaced it — the Rigor command and the finding, or the documentation sweep.
201
+
202
+ ## Verification
203
+
204
+ The witness for each change that says less: the call, and the runtime type checker's error under the old declaration (it passes under the new one). The declaration-mutation result (killed / survived) and every survivor, named as unchecked at runtime. The test added, and the `bin/test gems/NAME/X.Y` result for each version.
205
+ ```
206
+
207
+ Write each paragraph and list item on one line; GitHub renders a newline inside a paragraph as a line
208
+ break. "Deliberately left unchanged" lists every Rigor finding you did not act on and every `void`,
209
+ `boolish` or wide declaration kept over a body that returns more: it is the evidence that the restraint
210
+ was a decision.
@@ -0,0 +1,182 @@
1
+ # 05 — Declaration mutants: which declarations does any run actually check?
2
+
3
+ A runtime witness proves something only if the checker looked. RBS's runtime checker silently skips
4
+ some declarations: it checks a method only against a declaration on the same class or module that
5
+ defines the method in Ruby, so a declaration inherited from a module or superclass by a class that
6
+ defines or overrides the method is skipped (unless it carries `%a{rbs:test:target}`). In a field trial
7
+ on rgl 0.6.7, rgl's own test suite ran under `rbs/test/setup` with one expected failure while several
8
+ declarations of that kind were never checked. A single witness has the same problem: a clean exit means
9
+ nothing until a deliberately broken declaration makes the same run fail.
10
+
11
+ This is mutation testing aimed at the declarations instead of the code. Break one method return at a
12
+ time, re-run the witnesses, and see whether the run notices:
13
+
14
+ - **killed** — the run reports a type error, naming the mutated method or one of its RBS aliases, that
15
+ the clean run did not. The return is checked.
16
+ - **SURVIVED** — no such error. Nothing you ran checks this return: no call reaches it, or the checker
17
+ skips it. Treat it as unverified at runtime, whatever the suite reported.
18
+ - **ERROR** — the mutant does not load. Nothing was learned; fix the script or the declarations.
19
+ - **UNKNOWN** — the clean run already reports a return error on this method. Fix that first.
20
+
21
+ | Operator | Mutation | A survivor means |
22
+ | --- | --- | --- |
23
+ | `bot` | the return type becomes `bot`, which no value inhabits | no run checks this return at all |
24
+ | `drop-nil` | a `T?` return becomes `(T)` | no run reaches the `nil` arm, so the `?` has no witness |
25
+
26
+ `void` returns are skipped (the checker does not compare them, by design), and so is `drop-nil` on
27
+ `untyped?` (`untyped` already admits `nil`). Parameters, blocks and `attr_*` declarations are not mutated:
28
+ the pass says nothing about them.
29
+
30
+ A built-in command for this is proposed in #3072; until it exists, the script below is the procedure.
31
+
32
+ ## Running it
33
+
34
+ The script needs only the `rbs` gem. Give it the directory of declarations under review and the
35
+ command that runs your witnesses — a witness script, the project's test suite, or a collection entry's
36
+ `_test/` — under `rbs/test/setup` with `RBS_TEST_TARGET` set. The script sets `RBS_TEST_OPT` to the
37
+ mutant directory itself; pass any further `-I` / `-r` options in `RBS_DECL_MUTANTS_EXTRA_OPT`. Save it as
38
+ `rbs_decl_mutants.rb` outside the project.
39
+
40
+ ```sh
41
+ RBS_DECL_MUTANTS_EXTRA_OPT='-I ../deps-sig' ruby rbs_decl_mutants.rb sig -- ./run-witnesses.sh
42
+ ```
43
+
44
+ `run-witnesses.sh` is whatever runs your witnesses, for example:
45
+
46
+ ```sh
47
+ #!/bin/sh
48
+ RBS_TEST_TARGET='MyGem::*' ruby -I lib -I test -rrbs/test/setup -e 'Dir["test/*_test.rb"].each { |f| require_relative File.expand_path(f) }'
49
+ ```
50
+
51
+ Each mutant is one full run, so the pass costs the command's runtime times the number of non-`void`
52
+ returns (rgl: 61 mutants, about 70 seconds). It runs the library's code, so the safety rules for a
53
+ witness apply: for a gem not already in the user's bundle, use a container or VM or ask first.
54
+
55
+ ```ruby
56
+ # frozen_string_literal: true
57
+
58
+ # Break each RBS method return in turn and check whether a witness command notices.
59
+ #
60
+ # ruby rbs_decl_mutants.rb SIG_DIR -- COMMAND...
61
+ #
62
+ # COMMAND runs the witnesses under rbs/test/setup (RBS_TEST_TARGET set) and must read declarations from
63
+ # $RBS_TEST_OPT, which this script sets to `-I <mutant dir>` plus RBS_DECL_MUTANTS_EXTRA_OPT.
64
+ # Operators: `bot` (nothing inhabits it: is this declaration checked at all?) and `drop-nil` (`T?` -> `T`:
65
+ # does any witness reach the nil arm?). A mutant is killed only by a type error that names the mutated
66
+ # method and that the clean run did not report; a mutant that does not load is an ERROR, never a survivor;
67
+ # a method whose return the clean run already fails on is UNKNOWN.
68
+ require "rbs"
69
+ require "rbs/cli"
70
+ require "optparse"
71
+ require "fileutils"
72
+ require "open3"
73
+ require "set"
74
+ require "shellwords"
75
+ require "tmpdir"
76
+
77
+ Encoding.default_external = Encoding::UTF_8
78
+ sig_dir = File.expand_path(ARGV.shift.to_s)
79
+ ARGV.shift if ARGV.first == "--"
80
+ command = ARGV
81
+ abort "usage: ruby rbs_decl_mutants.rb SIG_DIR -- COMMAND..." if !File.directory?(sig_dir) || command.empty?
82
+ extra = ENV.fetch("RBS_DECL_MUTANTS_EXTRA_OPT", "")
83
+
84
+ # `[Owner#name] KindError: ...` -> ["Owner#name", "KindError"]; the value after the colon is dropped
85
+ # because it carries object addresses and sampled elements that differ from run to run.
86
+ ERROR_TAG = /^.*?\[(.+?)\] (\w+Error):/
87
+ RETURN_KINDS = %w[ReturnTypeError UnresolvedOverloadingError].freeze
88
+ def type_errors(out) = out.each_line.filter_map { |l| (m = l.match(ERROR_TAG)) && [m[1], m[2]] }.to_set
89
+
90
+ def run(command, dir, extra)
91
+ out, = Open3.capture2e({ "RBS_TEST_OPT" => "-I #{dir} #{extra}".strip }, *command, stdin_data: "")
92
+ out
93
+ end
94
+
95
+ # Load the mutant exactly as rbs/test/setup parses RBS_TEST_OPT, so one that does not load is reported.
96
+ def loads?(dir, extra)
97
+ options = RBS::CLI::LibraryOptions.new
98
+ OptionParser.new { |o| options.setup_library_options(o) }.parse!(Shellwords.split("-I #{dir} #{extra}"))
99
+ env = RBS::Environment.from_loader(options.loader).resolve_type_names
100
+ RBS::DefinitionBuilder.new(env: env)
101
+ true
102
+ rescue StandardError => e
103
+ warn " load error: #{e.class}: #{e.message.lines.first}"
104
+ false
105
+ end
106
+
107
+ files = Dir[File.join(sig_dir, "**", "*.rbs")].reject { |f| f.split(File::SEPARATOR).include?("_test") }
108
+ sites = []
109
+ collect = lambda do |decl, ns, file, aliases = Hash.new { [] }|
110
+ case decl
111
+ when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
112
+ # The checker reports a call made through an alias under the alias's name, so a kill may carry it.
113
+ aliases = Hash.new { |h, k| h[k] = [] }
114
+ decl.members.grep(RBS::AST::Members::Alias).each do |a|
115
+ aliases[[a.kind, a.old_name]] << a.new_name
116
+ aliases[[a.kind, a.new_name]] << a.old_name
117
+ end
118
+ decl.members.each { |m| collect.call(m, ns + [decl.name.to_s.delete_prefix("::")], file, aliases) }
119
+ when RBS::AST::Members::MethodDefinition
120
+ sep = decl.kind == :singleton ? "." : "#"
121
+ names = [decl.name, *aliases[[decl.kind == :singleton ? :singleton : :instance, decl.name]]]
122
+ decl.overloads.each_with_index do |overload, index|
123
+ ret = overload.method_type.type.return_type
124
+ next if ret.location.nil? || ret.is_a?(RBS::Types::Bases::Void)
125
+ label = "#{ns.join('::')}#{sep}#{decl.name} (overload #{index}, #{ret})"
126
+ tag_suffix = names.map { |n| "#{sep}#{n}" }
127
+ sites << [file, ret.location, "bot", label, tag_suffix]
128
+ next unless ret.is_a?(RBS::Types::Optional) && !ret.type.is_a?(RBS::Types::Bases::Any)
129
+ sites << [file, ret.location, "(#{ret.type})", "#{label} drop-nil", tag_suffix]
130
+ end
131
+ end
132
+ end
133
+ files.each { |f| RBS::Parser.parse_signature(File.read(f, encoding: "UTF-8"))[2].each { |d| collect.call(d, [], f) } }
134
+
135
+ Dir.mktmpdir("rbs-decl-mutants") do |tmp|
136
+ clean = File.join(tmp, "clean")
137
+ FileUtils.cp_r(sig_dir, clean)
138
+ FileUtils.rm_rf(Dir[File.join(clean, "**", "_test")])
139
+ abort "the clean declarations do not load" unless loads?(clean, extra)
140
+ baseline = type_errors(run(command, clean, extra))
141
+ warn "clean run reports #{baseline.size} type error(s); those tags are ignored" unless baseline.empty?
142
+
143
+ tally = Hash.new(0)
144
+ sites.each do |file, loc, replacement, label, tag_suffix|
145
+ dir = File.join(tmp, "mutant")
146
+ FileUtils.rm_rf(dir)
147
+ FileUtils.cp_r(clean, dir)
148
+ target = File.join(dir, file.delete_prefix(sig_dir).delete_prefix(File::SEPARATOR))
149
+ src = File.read(file, encoding: "UTF-8") # RBS locations are character offsets
150
+ File.write(target, src[0...loc.start_pos] + replacement + src[loc.end_pos..])
151
+ verdict =
152
+ if baseline.any? { |tag, kind| RETURN_KINDS.include?(kind) && tag.end_with?(*tag_suffix) }
153
+ "UNKNOWN" # the clean run already fails on this method; fix that first
154
+ elsif !loads?(dir, extra)
155
+ "ERROR"
156
+ else
157
+ fresh = type_errors(run(command, dir, extra)) - baseline
158
+ fresh.any? { |tag, _kind| tag.end_with?(*tag_suffix) } ? "killed" : "SURVIVED"
159
+ end
160
+ tally[verdict] += 1
161
+ puts "#{verdict.ljust(8)} #{label}"
162
+ end
163
+ puts "#{sites.size} mutants: #{tally['killed']} killed, #{tally['SURVIVED']} survived, " \
164
+ "#{tally['ERROR']} did not load, #{tally['UNKNOWN']} already failing in the clean run"
165
+ end
166
+ ```
167
+
168
+ ## Reading the result
169
+
170
+ - Report every survivor in the write-up as "unchecked at runtime", with the operator. It is not a
171
+ defect in the declaration; it is the limit of what your evidence covers.
172
+ - A `bot` survivor on a method your witnesses call is the checker's skip rule at work. Redeclare the
173
+ member on the class that defines the method (where that is also a correct declaration), add
174
+ `%a{rbs:test:target}` to the inherited declaration, or rely on documentation for it and say so.
175
+ - A `bot` survivor on a method nothing calls is a missing witness: add a call, or say so.
176
+ - A `drop-nil` survivor means the `?` rests on documentation alone. If the documentation does not
177
+ mention `nil` either, the `?` has no evidence: re-examine it.
178
+ - A killed mutant on a "say less" change is the negative control that change needs.
179
+
180
+ On the rgl draft the pass reported 61 mutants: 56 killed, 5 survived. Three survivors were the skip
181
+ rule (`Graph#directed?`, `#has_vertex?`, `#has_edge?`, declared on the module `Graph` and overridden in
182
+ classes); `Graph#to_s` was never called; and the `?` on `Edge::DirectedEdge#<=>` had no witness.
@@ -82,7 +82,9 @@ rbs --version # or: bundle exec rbs --version
82
82
  If it is missing, add it to the project's development group
83
83
  (`bundle add rbs --group development`) or install it standalone
84
84
  (`gem install rbs`). Prefer `bundle exec rbs …` when `rbs` is in the
85
- Gemfile so it matches the project's resolved versions.
85
+ Gemfile so it matches the project's resolved versions. If `bundle exec rbs`
86
+ runs an `rbs` that a mise-installed `gem:rigortype` put on `PATH`, see the
87
+ [install guide's note on `rbs` shadowing](https://github.com/rigortype/rigor/blob/master/docs/install.md#case-a--mise-is-available-recommended).
86
88
 
87
89
  ### Phase 3 — initialise the collection config (first time only)
88
90
 
@@ -69,6 +69,9 @@ scope the moment you are about to write or assert one:
69
69
  - **Working an existing `.rigor-baseline.yml` down** →
70
70
  `rigor-baseline-reduce`. That is fixing diagnostics, not sourcing a
71
71
  type.
72
+ - **Auditing, repairing, or contributing RBS declarations** — a gem's
73
+ `sig/`, an application's `sig/`, a gem_rbs_collection entry →
74
+ `rigor-rbs-contract`. This skill still governs every type it writes.
72
75
  - **Raising type protection from a `coverage --protection` list** →
73
76
  `rigor-protection-uplift`. It owns the "where should a type go, and how
74
77
  do I verify it" loop; this skill owns "where does the type itself come
@@ -76,7 +79,9 @@ scope the moment you are about to write or assert one:
76
79
 
77
80
  ## The three allowed sources
78
81
 
79
- Every type you write comes from exactly one of these. There is no fourth.
82
+ Every type you write comes from exactly one of these. There is no fourth
83
+ source for a type itself; what a *declaration* promises is the separate
84
+ contract question below.
80
85
 
81
86
  | What you need | Ask Rigor with | What you write |
82
87
  | --- | --- | --- |
@@ -96,6 +101,26 @@ adopting, under the gate that `rigor check` gains no new diagnostic.
96
101
  Exact command forms, flags, position syntax, JSON shapes, and how to read
97
102
  each output: [`references/01-oracle-commands.md`](references/01-oracle-commands.md).
98
103
 
104
+ ## A declaration is a contract, not a missing type
105
+
106
+ None of the three sources says what a declaration *promises* to callers.
107
+ Ruby returns every method's last expression, so a body almost always has
108
+ a value; a declared `void` says callers must not depend on it, and
109
+ `boolish` says only its truthiness counts. **Never tighten `void`,
110
+ `boolish`, or a declared type deliberately wider than the body (anything
111
+ but `untyped`) to what the body happens to return** — in `sig/`, in an
112
+ inline annotation, or in RBS sent upstream — unless the library documents
113
+ that value or, for the project's own code, the owner decides. A
114
+ `sig-gen` proposal over `untyped` is not this case; it is the second
115
+ source above.
116
+
117
+ The reverse also holds. A contract the body does not prove — `String?`
118
+ because the method's documentation says it returns `nil` when a job is
119
+ halted — is not a guess when it is cited: the library's documentation,
120
+ quoted, for code you do not own; the owner's explicit answer for code
121
+ they do. Auditing, repairing, or contributing a set of RBS declarations
122
+ on that basis is **`rigor-rbs-contract`**.
123
+
99
124
  ## The gap protocol — a gap is a finding, not a blank to fill
100
125
 
101
126
  `Dynamic[top]`, `untyped`, and a `sig.skipped.*` classification are
@@ -17,7 +17,7 @@ the project is already onboarded, add it now.
17
17
  only if it is not already pulled in.
18
18
  - **Neither exists** → create `AGENTS.md` with this as its first section.
19
19
  - **A "Types" / "Type checking" section already exists** → merge into it
20
- rather than adding a second one, and keep the five bullets intact.
20
+ rather than adding a second one, and keep the six bullets intact.
21
21
 
22
22
  Never overwrite unrelated content, and never rewrite a rule the project
23
23
  already wrote for itself — show the user the conflict instead.
@@ -27,13 +27,14 @@ already wrote for itself — show the user the conflict instead.
27
27
  ```markdown
28
28
  ## Types come from Rigor, not from reading code
29
29
 
30
- This project is type-checked by [Rigor](https://github.com/rigortype/rigor). A type you did not obtain from Rigor is a guess, and a guessed type is never written anywhere: not in `sig/`, not in an inline annotation (`#:`, `# @rbs`), not in a doc comment, not in a review comment, and not as the reason for a nil check or an `is_a?` guard.
30
+ This project is type-checked by [Rigor](https://github.com/rigortype/rigor). A type you did not obtain from Rigor is a guess, and a guessed type is never written anywhere: not in `sig/`, not in an inline annotation (`#:`, `# @rbs`), not in a doc comment, not in a review comment, and not as the reason for a nil check or an `is_a?` guard. The one other source is a contract: what a declaration promises callers may rest on the library's documentation, quoted, or for this project's own code on the owner's explicit decision.
31
31
 
32
32
  - The type of an expression: `rigor type-of FILE:LINE:COL`, or `rigor annotate FILE` for a whole file.
33
33
  - The signature of a method: `rigor sig-gen --print FILE`; paste what it prints, never what you expect.
34
34
  - A parameter type is the one thing inference does not give you: derive it from the call sites with `rigor sig-gen --observe PATH` and keep it only while `rigor check` stays green.
35
- - When Rigor answers `Dynamic[top]` or `untyped`, or `sig-gen` skips the method, do not fill the gap. Report the exact command and its output; the gap is the finding.
35
+ - When Rigor answers `Dynamic[top]` or `untyped`, or `sig-gen` skips the method, do not fill the gap from your reading. Report the exact command and its output; the gap is the finding. Only a contract source, cited as above, fills it.
36
36
  - Every type you state to a human carries the command that produced it, so it can be re-run.
37
+ - A declared `void` or `boolish` return, or a declared type deliberately wider than the body (anything but `untyped`), is a contract, not a missing type: never tighten it to what the body happens to return unless the library documents that value or, for this project's own code, the owner decides. Auditing or repairing RBS declarations is the `rigor-rbs-contract` skill.
37
38
 
38
39
  The `rigor-type-oracle` skill (`rigor skill --full rigor-type-oracle`) has the full procedure. With the Rigor MCP server connected, `rigor_type_of`, `rigor_annotate`, and `rigor_sig_gen` are the same oracle as tool calls.
39
40
  ```