docscribe 1.6.1 → 1.6.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -193
  3. data/exe/docscribe-client +26 -7
  4. data/lib/docscribe/cli/config_builder.rb +37 -2
  5. data/lib/docscribe/cli/coverage.rb +5 -5
  6. data/lib/docscribe/cli/formatters/json.rb +74 -29
  7. data/lib/docscribe/cli/formatters/sarif.rb +20 -3
  8. data/lib/docscribe/cli/options.rb +17 -2
  9. data/lib/docscribe/cli/rbs_gen.rb +4 -4
  10. data/lib/docscribe/cli/run.rb +107 -24
  11. data/lib/docscribe/cli/update_types.rb +61 -17
  12. data/lib/docscribe/cli.rb +19 -13
  13. data/lib/docscribe/config/defaults.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +22 -1
  15. data/lib/docscribe/config/template.rb +3 -0
  16. data/lib/docscribe/config/validation.rb +19 -0
  17. data/lib/docscribe/config.rb +1 -0
  18. data/lib/docscribe/infer/behavior.rb +13 -13
  19. data/lib/docscribe/infer/params.rb +2 -2
  20. data/lib/docscribe/infer/raises.rb +5 -6
  21. data/lib/docscribe/infer/returns.rb +1612 -151
  22. data/lib/docscribe/infer.rb +7 -7
  23. data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
  24. data/lib/docscribe/inline_rewriter.rb +263 -97
  25. data/lib/docscribe/plugin/registry.rb +1 -0
  26. data/lib/docscribe/server/base.rb +46 -15
  27. data/lib/docscribe/server/client.rb +20 -11
  28. data/lib/docscribe/server/daemon.rb +200 -23
  29. data/lib/docscribe/server/protocol.rb +4 -4
  30. data/lib/docscribe/types/primitive.rb +160 -0
  31. data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
  32. data/lib/docscribe/types/yard/formatter.rb +35 -6
  33. data/lib/docscribe/types/yard/parser.rb +25 -20
  34. data/lib/docscribe/types/yard/validator.rb +131 -0
  35. data/lib/docscribe/validator/generic_compatibility.rb +698 -0
  36. data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
  37. data/lib/docscribe/version.rb +1 -1
  38. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24df08456004986c5cae869a2e7dc9ba4a1176cbf5dbaaca679cb4661836a3dc
4
- data.tar.gz: 746d064972fe321f6b0d7459d87f1fc4cec8c414b4b4b73b9cce916f72b4139d
3
+ metadata.gz: 3c7164ab1529448e4446137381b717690e22d57ced3cf9042be38ab00ff5f470
4
+ data.tar.gz: 7dc01723974b29fe8a6179220d04ffe3c9141e471c3ec1fbed309b9e5a780a5e
5
5
  SHA512:
6
- metadata.gz: 4cf1ba70c9a43ec923abda6c16bbd599b287803fd9c6968b0e4c0ea87079fd8d1dd95317f7a20715f6664a7a1876da46cf008b3f6c21fc115237592b041f29b2
7
- data.tar.gz: 0e7e994de57e167edf3a47b3d92ad1c123cb2decf973d137fcc7ac0bcd33f869fc380abd2d2705a2820d09837bce012b12f2f7efc2bbf0fe679da226040e43fb
6
+ metadata.gz: cee99e8d318b327bc64786555c0347cc995e6a23d5fb42863c13efda6e6467bdc5032bd37f1a7b518ca3f4c41c5967034f278664f0b9ee1110d712f01011f000
7
+ data.tar.gz: 59ab6f4b18a349fc9e40c3b39f85dd331c76f606bb9162ff410b11e40338f9a3e3d05e8ab002e345c6ef3be47a1ee524cc9d12649ce7d0a7deb149c376c6edd5
data/README.md CHANGED
@@ -78,6 +78,7 @@ docscribe -A lib
78
78
  * [Installation](#installation)
79
79
  * [Architecture](#architecture)
80
80
  * [Data flow](#data-flow)
81
+ * [IDE plugin and gem interaction](#ide-plugin-and-gem-interaction)
81
82
  * [CLI](#cli)
82
83
  * [Exit codes](#exit-codes)
83
84
  * [Options](#options)
@@ -175,193 +176,31 @@ In server mode, a persistent daemon (`docscribe server`) keeps the runtime loade
175
176
  invocations via an LRU cache, enabling near-instant repeated checks for IDE plugins. A thin client (`docscribe-client`)
176
177
  provides minimal-overhead socket communication without loading the full gem.
177
178
 
178
- ```mermaid
179
- flowchart TB
180
- subgraph CLI["CLI Layer"]
181
- Exe["exe/docscribe\nEntry point"]
182
- Run["CLI::Run\nMain execution\n· expand paths\n· iterate files\n· report results"]
183
- Options["CLI::Options\nARGV parsing\n(mode, strategy,\nfilters, flags)"]
184
- InitCmd["CLI::Init\ndocscribe init\nGenerate config"]
185
- GenCmd["CLI::Generate\ndocscribe generate\nScaffold plugins"]
186
- SigsCmd["CLI::Sigs\ndocscribe sigs\nCheck RBS coverage"]
187
- RbsGenCmd["CLI::RbsGen\ndocscribe rbs\nGenerate RBS from YARD"]
188
- SarifFormatter["CLI::Formatters::Sarif\nSARIF 2.1 JSON\nCode Scanning"]
189
- ConfigBuilder["CLI::ConfigBuilder\nApply CLI overrides\nto config"]
190
- end
191
-
192
- subgraph Config["Configuration"]
193
- ConfigClass["Config\nCentral config object\n· raw hash\n· query methods"]
194
- Defaults["config/defaults.rb\nDEFAULT hash"]
195
- Loader["config/loader.rb\nYAML loading\n+ deep merge"]
196
- Emit["config/emit.rb\nEmission toggles\n(header, tags, etc.)"]
197
- Filtering["config/filtering.rb\nFile/method\ninclude/exclude"]
198
- RBSConfig["config/rbs.rb\nRBS provider\nfactory"]
199
- SorbetConfig["config/sorbet.rb\nSorbet provider\nchain factory"]
200
- PluginConfig["config/plugin.rb\nPlugin loading\nfrom YAML"]
201
- end
179
+ <img src="assets/diagrams/architecture.svg" alt="Docscribe architecture">
202
180
 
203
- subgraph Parsing["Parsing"]
204
- ParsingModule["Parsing\nBackend selection\n(:parser / :prism)"]
205
- ParserGem["Parser gem\n(whitequark/parser)"]
206
- Prism["Prism translator\n(Ruby 3.4+)"]
207
- end
181
+ Source: [assets/diagrams/architecture.mmd](assets/diagrams/architecture.mmd) — regenerate with `bash assets/diagrams/render.sh`.
208
182
 
209
- subgraph Core["Core Engine"]
210
- InlineRewriter["InlineRewriter\n· parse -> collect\n· deduplicate -> dispatch\n· rewrite"]
211
- Collector["Collector\n< Parser::AST::Processor\nAST walker\n· find methods/attrs\n· track visibility\n· track containers"]
212
- DocBuilder["DocBuilder\nGenerate YARD doc lines\n· combine inference\n· external signatures\n· plugin tags"]
213
- DocBlock["DocBlock\nSafe strategy:\nparse -> merge -> sort\nexisting doc blocks"]
214
- SourceHelpers["SourceHelpers\nPosition/range\nutilities"]
215
- end
183
+ ### Data flow
216
184
 
217
- subgraph Infer["Inference Engine"]
218
- InferModule["Infer\nEntry point"]
219
- Params["Infer::Params\nParameter type\nfrom name + default"]
220
- Returns["Infer::Returns\nReturn type\nfrom method body"]
221
- Raises["Infer::Raises\n@raise tags\nfrom raise/rescue"]
222
- Literals["Infer::Literals\nAST literal ->\ntype string"]
223
- Names["Infer::Names\n:const node ->\nFQN string"]
224
- ASTWalk["Infer::ASTWalk\nRecursive DFS\nAST traversal"]
225
- end
185
+ <img src="assets/diagrams/dataflow.svg" alt="Docscribe data flow">
226
186
 
227
- subgraph Plugins["Plugin System"]
228
- PluginModule["Plugin\nTag/Collector\ndispatch"]
229
- Registry["Plugin::Registry\nGlobal registry\n· register -> route\n· tag_entries\n· collector_entries"]
230
- TagPlugin["Base::TagPlugin\nOverride #call(context)\n-> Array<Tag>"]
231
- CollectorPlugin["Base::CollectorPlugin\nOverride #collect(ast, buffer)\n-> Array<Hash>"]
232
- TagValue["Plugin::Tag\nStruct (name, text, types)"]
233
- Context["Plugin::Context\nMethod snapshot struct"]
234
- end
187
+ Source: [assets/diagrams/dataflow.mmd](assets/diagrams/dataflow.mmd) — regenerate with `bash assets/diagrams/render.sh`.
235
188
 
236
- subgraph Types["External Type System"]
237
- ProviderChain["ProviderChain\nComposite:\nquery in order\nfirst match wins"]
238
- RBSProvider["RBS::Provider\n.rbs files\n-> RBS lib"]
239
- RBSFormatter["RBS::TypeFormatter\nRBS type ->\nYARD type string"]
240
- RBSCollection["RBS::CollectionLoader\nrbs_collection\n.lock.yaml"]
241
- SorbetBase["Sorbet::BaseProvider\nRBS::Prototype::RBI\nbridge"]
242
- SorbetSource["Sorbet::SourceProvider\nInline sig{}\ndeclarations"]
243
- SorbetRBI["Sorbet::RBIProvider\n.rbi files\ndirectories"]
244
- end
189
+ ### IDE plugin and gem interaction
245
190
 
246
- subgraph YardTypes["YARD Type Parser"]
247
- YParser["Yard::Parser\nParse YARD type\nstrings -> AST"]
248
- YFormatter["Yard::Formatter\nYARD AST ->\nRBS string"]
249
- YTypes["Yard::Types\n9 AST node types\n(Named, Generic, etc.)"]
250
- end
191
+ <img src="assets/diagrams/plugin-gem.svg" alt="RubyMine plugin and gem interaction">
251
192
 
252
- subgraph Server["Server / Daemon"]
253
- ThinClient["exe/docscribe-client\nThin client\n· socket send/receive\n· no gem load"]
254
- ServerDaemon["Server::Daemon\nSocket listener\n· check / fix / shutdown\n· JSON-RPC 2.0"]
255
- Cache["Docscribe::LRUCache\nFile result cache\n(max 1000, by mtime)"]
256
- end
193
+ Source: [assets/diagrams/plugin-gem.mmd](assets/diagrams/plugin-gem.mmd) — regenerate with `bash assets/diagrams/render.sh`.
257
194
 
258
- Exe --> Run
259
- Exe --> ServerDaemon
260
- ThinClient --> ServerDaemon
261
- ServerDaemon --> ConfigClass
262
- ServerDaemon --> Cache
263
- ServerDaemon --> InlineRewriter
264
- Run --> Options
265
- Run --> InitCmd
266
- Run --> GenCmd
267
- Run --> SigsCmd
268
- Run --> RbsGenCmd
269
- Run --> SarifFormatter
270
- Run --> ConfigBuilder
271
- ConfigBuilder --> ConfigClass
272
- ConfigBuilder --> ServerDaemon
273
- ConfigClass --> Defaults
274
- ConfigClass --> Loader
275
- ConfigClass --> Emit
276
- ConfigClass --> Filtering
277
- ConfigClass --> RBSConfig
278
- ConfigClass --> SorbetConfig
279
- ConfigClass --> PluginConfig
280
- Run --> InlineRewriter
281
- InlineRewriter --> ParsingModule
282
- ParsingModule --> ParserGem
283
- ParsingModule --> Prism
284
- InlineRewriter --> Collector
285
- Collector --> PluginModule
286
- PluginModule --> Registry
287
- Registry --> CollectorPlugin
288
- InlineRewriter --> DocBuilder
289
- DocBuilder --> InferModule
290
- InferModule --> Params
291
- InferModule --> Returns
292
- InferModule --> Raises
293
- Params --> Literals
294
- Returns --> Literals
295
- Raises --> ASTWalk
296
- Raises --> Names
297
- DocBuilder --> ProviderChain
298
- ProviderChain --> SorbetSource
299
- ProviderChain --> SorbetRBI
300
- ProviderChain --> RBSProvider
301
- SorbetSource --> SorbetBase
302
- SorbetRBI --> SorbetBase
303
- RBSProvider --> RBSFormatter
304
- RBSProvider --> RBSCollection
305
- DocBuilder --> PluginModule
306
- PluginModule --> Registry
307
- Registry --> TagPlugin
308
- TagPlugin --> TagValue
309
- TagPlugin --> Context
310
- InlineRewriter --> DocBlock
311
- InlineRewriter --> SourceHelpers
312
- RbsGenCmd --> ParsingModule
313
- RbsGenCmd --> YParser
314
- YParser --> YTypes
315
- YParser --> YFormatter
316
- ```
195
+ How it works, briefly: the IDE annotator collects file info on open/type/save, asks the gem daemon (`check` over Unix-socket JSON-RPC, CLI fallback), maps offenses to YARD tag lines with per-cop quick-fixes, and caches by file stamp + config hash. Daemon errors surface as file-level errors instead of silence; quick-fixes run `fix` or file-scoped `update_types`; workspace check aggregates `check_batch` into a balloon.
317
196
 
318
- ### Data flow
197
+ Version gates (plugin behavior by gem version):
319
198
 
320
- ```mermaid
321
- flowchart LR
322
- subgraph Entry["Entry Points"]
323
- Direct["docscribe lib\n(no --server)"]
324
- ViaServer["docscribe --server\nor docscribe-client"]
325
- end
326
-
327
- subgraph Daemon["Server Daemon (Unix Socket)"]
328
- Socket["Daemon#listen_loop\nJSON-RPC 2.0 dispatch"]
329
- CacheCheck{"File cached &\nmtime fresh?"}
330
- CacheStorage["LRUCache\n(1000 entries)"]
331
- ApplyOverrides["apply_cli_overrides\n(reset on nil)"]
332
- end
333
-
334
- ViaServer --> Socket
335
- Socket --> ApplyOverrides
336
- ApplyOverrides --> CacheCheck
337
- CacheCheck -->|Hit| Socket
338
- CacheCheck -->|Miss| Parse
339
- Direct --> Parse
340
- Parse["Parsing.parse_buffer\nParser gem / Prism"]
341
- Parse --> AST["AST + Comments"]
342
- AST --> Collect["Collector.process\n· Find methods\n· Track visibility\n· Find attr_*"]
343
- AST --> CollectPlugins["CollectorPlugin#collect\n· Custom AST walks\n· Non-standard constructs"]
344
- Collect --> Insertions["Insertion list\n(sorted by position)"]
345
- CollectPlugins --> Insertions
346
- Insertions --> Dedup["Deduplicate\n(override by position)"]
347
- Dedup --> Build["DocBuilder.build_doc_lines\nper insertion"]
348
- Build --> Infer["Infer params / returns / raises\n(heuristic fallback)"]
349
- Build --> SigQuery["ProviderChain\nquery external types"]
350
- Build --> TagPlugins["TagPlugin#call\n(add extra @tags)"]
351
- Infer --> ResultDoc["Generated YARD doc block"]
352
- SigQuery --> ResultDoc
353
- TagPlugins --> ResultDoc
354
- ResultDoc --> Strategy{"Strategy?"}
355
- Strategy -->|Safe| Merge["DocBlock.merge\npreserve + append + sort"]
356
- Strategy -->|Aggressive| Replace["Replace entirely"]
357
- Merge --> Rewritten["Rewriter#process\n-> rewritten source"]
358
- Replace --> Rewritten
359
- Rewritten --> Result["Result / response"]
360
- Rewritten --> CacheStorage
361
- CacheStorage --> Socket
362
- Result -->|Direct mode| Output["Modified .rb file / STDOUT"]
363
- Result -->|Server mode| Socket
364
- ```
199
+ | Gem version | Plugin behavior |
200
+ |-------------|-------------------------------------------------------------|
201
+ | `< 1.5.1` | always CLI, no daemon mode |
202
+ | `< 1.5.2` | no batch mode (workspace check scans the directory via CLI) |
203
+ | `< 1.6.2` | `update_types` falls back to CLI on unknown method |
365
204
 
366
205
  ## CLI
367
206
 
@@ -371,7 +210,7 @@ docscribe init [options]
371
210
  docscribe generate [type] [name] [options]
372
211
  docscribe sigs [options] [files...]
373
212
  docscribe rbs [options] [files...]
374
- docscribe update_types [directory]
213
+ docscribe update_types [directory|file] [options]
375
214
  docscribe check_for_comments [paths...]
376
215
  docscribe server [start|status|stop] [options]
377
216
  ```
@@ -437,6 +276,10 @@ If you pass no files and don't use `--stdin`, Docscribe processes the current di
437
276
  - `--format FORMAT`
438
277
  Output format: `text` (default, human-readable), `json` (machine-readable, RuboCop-compatible), or `sarif` (SARIF 2.1
439
278
  JSON, compatible with GitHub Code Scanning).
279
+ Every JSON offense carries `cop_name`, `message`, `location`, plus machine-readable `type`
280
+ (`updated_return`, `missing_param`, ...) and, for type mismatches, `source` (`"rbs"`, `"infer"` or `"syntax"`).
281
+ Files are grouped by normalized path, so the same file passed under different spellings
282
+ (absolute, relative, symlinked) appears once.
440
283
 
441
284
  - `--rbs`
442
285
  Use RBS signatures for `@param`/`@return` when available (falls back to inference).
@@ -450,6 +293,10 @@ If you pass no files and don't use `--stdin`, Docscribe processes the current di
450
293
  - `--rbi-dir DIR`
451
294
  Add an Sorbet RBI directory (repeatable). Implies `--sorbet`.
452
295
 
296
+ - `--[no-]validate-types`
297
+ Validate YARD types against inferred/RBS types and report mismatches.
298
+ `--no-validate-types` overrides `validate_types: true` from the config file.
299
+
453
300
  - `--include PATTERN`
454
301
  Include PATTERN (method id or file path; glob or `/regex/`).
455
302
 
@@ -611,21 +458,28 @@ end
611
458
  ### `docscribe update_types` — two-pass type-aware documentation update
612
459
 
613
460
  > [!NOTE]
614
- > `docscribe update_types` is a convenience alias for the two-pass workflow above. It requires Ruby 3.0+ and the `rbs`
615
- > gem (because of `--rbs-collection`). The RBS collection must be set up first with
616
- > `bundle exec rbs collection install`. Type accuracy depends on your RBS signatures — if signatures are incomplete or
617
- > missing, types will fall back to AST inference.
461
+ > `docscribe update_types` is a convenience alias for the two-pass workflow above. RBS features require Ruby 3.0+ and
462
+ > the `rbs` gem; pass `--no-rbs` to skip them. If `rbs.collection: true` is set in `docscribe.yml`, the collection is
463
+ > discovered automatically (otherwise run `bundle exec rbs collection install` first). Type accuracy depends on your
464
+ > RBS signatures — if signatures are incomplete or missing, types will fall back to AST inference.
618
465
 
619
- `docscribe update_types` runs two passes to bring both docs and RBS signatures up to date:
466
+ `docscribe update_types` runs two passes to bring both docs and RBS signatures up to date. The target can be a
467
+ directory **or a single file** (handy for IDE quick-fixes — only that file is touched):
620
468
 
621
469
  1. **Pass 1** — `docscribe -AkB --rbs-collection <dir>`: aggressively rebuilds doc blocks, preserves existing
622
470
  descriptions, suppresses boilerplate, uses RBS collection types.
623
471
  2. **Pass 2** — `docscribe -aB --rbs-collection <dir>`: safe merge cleanup with no boilerplate.
624
472
 
473
+ Extra flags (`--rbs`, `--sig-dir DIR` (repeatable), `--rbs-collection`, `--[no-]validate-types`, `--no-rbs`) are
474
+ passed through to both passes.
475
+
625
476
  ```shell
626
477
  # Update docs in lib/ using RBS collection
627
478
  docscribe update_types lib
628
479
 
480
+ # Update a single file only
481
+ docscribe update_types lib/user.rb
482
+
629
483
  # Defaults to current directory
630
484
  docscribe update_types
631
485
  ```
@@ -805,13 +659,17 @@ The daemon speaks JSON-RPC 2.0 over Unix socket. Each request is a JSON line, ea
805
659
 
806
660
  Methods:
807
661
 
808
- | Method | Parameters | Result |
809
- |---------------|-------------------------------------------------------------------------------------|---------------------------------------------------------|
810
- | `check` | `file` (string), `strategy` ("safe"/"aggressive"), `cli_overrides` (hash, optional) | `status`, `changed`, `changes` |
811
- | `fix` | same as `check` | `status`, `changed`, `changes` |
812
- | `check_batch` | `files` (array of strings), `strategy`, `cli_overrides`, `timeout` (int, optional) | array of per-file results |
813
- | `ping` | | `version`, `pid`, `socket_path`, `started_at`, `uptime` |
814
- | `shutdown` | — | `status` |
662
+ | Method | Parameters | Result |
663
+ |----------------|-------------------------------------------------------------------------------------|---------------------------------------------------------|
664
+ | `check` | `file` (string), `strategy` ("safe"/"aggressive"), `cli_overrides` (hash, optional) | `status`, `changed`, `changes` |
665
+ | `fix` | same as `check` | `status`, `changed`, `changes` |
666
+ | `check_batch` | `files` (array of strings), `strategy`, `cli_overrides`, `timeout` (int, optional) | array of per-file results |
667
+ | `update_types` | `file` (string, optional), `dir` (string, optional; `file` wins) | `status`, `changed`, `changes` |
668
+ | `ping` | — | `version`, `pid`, `socket_path`, `started_at`, `uptime` |
669
+ | `shutdown` | — | `status` |
670
+
671
+ Each entry in `changes` carries `type`, `line`, `message` and, for type mismatches, `source` (`"rbs"`, `"infer"` or
672
+ `"syntax"`) plus `param` where applicable — IDE plugins use `source` to pick the right fix (RBS update vs YARD fix).
815
673
 
816
674
  Error codes (standardized for IDE plugin integration):
817
675
 
@@ -931,11 +789,12 @@ Useful flag combinations for common workflows:
931
789
  projects.
932
790
  - `docscribe -a --sorbet --rbi-dir sorbet/rbi lib` — safe autocorrect using Sorbet RBI signatures.
933
791
  - `docscribe update_types lib` — two-pass type-aware update: aggressively rebuilds docs with kept descriptions and RBS
934
- collection, then safe-merges to clean up.
792
+ collection, then safe-merges to clean up. Accepts a single file too: `docscribe update_types lib/user.rb`.
935
793
  See [docscribe update_types](#docscribe-update_types--two-pass-type-aware-documentation-update).
936
794
 
937
795
  > [!NOTE]
938
- > `docscribe update_types` is a convenient shortcut, but be aware it uses `--rbs-collection` under the hood.
796
+ > `docscribe update_types` is a convenient shortcut, but be aware it uses `--rbs-collection` under the hood
797
+ > (or `rbs.collection: true` from the config — then no flag is needed).
939
798
  > If your RBS signatures are incomplete, types may fall back to AST inference.
940
799
 
941
800
  ## Parser backend (Parser gem vs Prism)
@@ -1096,6 +955,13 @@ You can combine `--rbs-collection` with `--sig-dir` to mix gem signatures with y
1096
955
  docscribe -a --rbs-collection --sig-dir sig lib
1097
956
  ```
1098
957
 
958
+ If `rbs.collection: true` is set in `docscribe.yml`, the collection is discovered automatically and the flag
959
+ can be omitted everywhere (including plain `check` and the daemon):
960
+
961
+ ```shell
962
+ docscribe -a lib
963
+ ```
964
+
1099
965
  > [!NOTE]
1100
966
  > `--rbs-collection` only improves types for methods defined in gems that ship RBS
1101
967
  > signatures. For your own classes, provide a `sig/` directory with hand-written or
@@ -1301,7 +1167,7 @@ Heuristics (best-effort).
1301
1167
  Parameters:
1302
1168
 
1303
1169
  - `*args` -> `Array`
1304
- - `**kwargs` -> `Hash`
1170
+ - `**kwargs` -> `Hash[Symbol, untyped]` with RBS, `Hash<Symbol, Object>` in YARD, bare `Hash` from pure inference
1305
1171
  - `&block` -> `Proc`
1306
1172
  - keyword args:
1307
1173
  - `verbose: true` -> `Boolean`
@@ -1316,6 +1182,18 @@ Return values:
1316
1182
  - For simple bodies, Docscribe looks at the last expression or explicit `return`.
1317
1183
  - Unions with `nil` become optional types (e.g. `String` or `nil` -> `String?`).
1318
1184
  - For control flow (`if`/`case`), it unifies branches conservatively.
1185
+ - Blocks resolve to generics where possible: `map`/`then` over known elements give `Array<String>` instead of
1186
+ bare `Array`; `arr << x` and `x += 1` resolve through RBS (`self`-returns stay `self`); `&.`/`||`/`()` receivers
1187
+ are unwrapped before lookup; `each_with_index` chains infer `Enumerator`/`Hash` element types
1188
+ (`arr.each_with_index.to_h { |x, i| [k, v] }` infers `Hash<K, V>` from the pair literal;
1189
+ the second block parameter is always the `Integer` index).
1190
+ - Generic compatibility is structural, not textual: `Hash` matches `Hash<Symbol, Config>`, `Array` matches
1191
+ `Array<String>`, `String?` equals `String, nil` and `String|nil`, and `String` is accepted where `Object` is
1192
+ expected. YARD `[]` and RBS `<>` (plus `untyped`/`Object`) are normalized before comparison.
1193
+ - `void` understands Ruby idioms: initializers and `setup` may return `Hash`/`self`/`Boolean`, predicates
1194
+ (`valid?`) may return `Boolean`.
1195
+ - One crashing method never blanks the whole file: the error goes to stderr (`Docscribe: skipping method foo ...`)
1196
+ and the remaining methods are still processed.
1319
1197
 
1320
1198
  > [!TIP]
1321
1199
  > Docscribe resolves return types for core Ruby methods (`Integer#positive?`, `String#upcase`, etc.)
@@ -1340,6 +1218,9 @@ Docscribe detects exceptions and rescue branches:
1340
1218
 
1341
1219
  - Conditional return types for rescue branches:
1342
1220
  - Docscribe adds `@return [Type] if ExceptionA, ExceptionB` for each rescue clause
1221
+ - Conditional tags never overwrite the main `@return` type, so `check` and `update_types` agree with each other
1222
+ (no ping-pong between runs). Rescue bodies referencing constants resolve to the constant's value type when
1223
+ the constant is visible from the method's scope.
1343
1224
 
1344
1225
  ## Visibility semantics
1345
1226
 
@@ -1889,6 +1770,7 @@ The generated file contains:
1889
1770
  |-------------------------------------------|------------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
1890
1771
  | `keep_descriptions` | `bool` | `false` | Preserve existing doc text in aggressive mode |
1891
1772
  | `skip_anonymous_block_params` | `bool` | `false` | Skip `@param [Proc] block` for anonymous `&` params |
1773
+ | `validate_types` | `bool` | `false` | Validate YARD types against inferred/RBS types and report mismatches |
1892
1774
  | `emit.header` | `bool` | `false` | Generate method header line (`+#foo+ -> ...`) |
1893
1775
  | `emit.include_default_message` | `bool` | `true` | Insert default message (`Method documentation.`) |
1894
1776
  | `emit.include_param_documentation` | `bool` | `true` | Insert param description text (`Param documentation.`) |
@@ -1998,6 +1880,7 @@ yard doc -o docs
1998
1880
  - Safe mode only merges into existing **doc-like** comment blocks. Ordinary comments that are not recognized as
1999
1881
  documentation are preserved and treated conservatively.
2000
1882
  - Type inference is heuristic. Complex flows and meta-programming will fall back to `Object` or best-effort types.
1883
+ - A method that crashes documentation generation is skipped with a stderr warning instead of failing the whole file.
2001
1884
  - Aggressive mode (`-A`) replaces existing doc blocks and should be reviewed carefully.
2002
1885
 
2003
1886
  ## Roadmap
data/exe/docscribe-client CHANGED
@@ -13,7 +13,8 @@ SOCKET_DIR = begin
13
13
  sock_overhead = "/docscribe-#{'a' * 32}.sock".bytesize
14
14
  tmp.bytesize <= 104 - sock_overhead ? tmp : '/tmp'
15
15
  end
16
- ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml].freeze
16
+ ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml docscribe.yml].freeze
17
+ SIG_RBS_GLOB = 'sig/**/*.rbs'
17
18
 
18
19
  def socket_path(config_path = nil)
19
20
  seed = +Dir.pwd
@@ -27,13 +28,27 @@ def socket_path(config_path = nil)
27
28
  end
28
29
 
29
30
  def env_hash
30
- parts = ENV_FILES.map do |file|
31
- path = File.join(Dir.pwd, file)
32
- File.exist?(path) ? File.mtime(path).to_f.to_s : '0'
33
- end
31
+ parts = ENV_FILES.map { |file| env_file_mtime(file) }
32
+ parts.concat(sig_env_parts)
34
33
  Digest::MD5.hexdigest(parts.join(':'))
35
34
  end
36
35
 
36
+ def env_file_mtime(file)
37
+ path = File.join(Dir.pwd, file)
38
+ File.exist?(path) ? File.mtime(path).to_f.to_s : '0'
39
+ end
40
+
41
+ def sig_env_parts
42
+ files = sig_files
43
+ mtimes = files.map { |p| File.mtime(p).to_f.to_s }
44
+ mtimes << files.size.to_s
45
+ mtimes
46
+ end
47
+
48
+ def sig_files
49
+ Dir.glob(File.join(Dir.pwd, SIG_RBS_GLOB)).sort
50
+ end
51
+
37
52
  def config_hash(config_path)
38
53
  resolved = File.expand_path(config_path)
39
54
  mtime = File.exist?(resolved) ? File.mtime(resolved).to_f : 0.0
@@ -141,8 +156,12 @@ when :ping
141
156
  puts JSON.generate(result || { error: 'Connection failed' })
142
157
  when :status
143
158
  alive = begin
144
- File.exist?(sock) &&
145
- UNIXSocket.new(sock).close && true
159
+ if File.exist?(sock)
160
+ UNIXSocket.new(sock).close
161
+ true
162
+ else
163
+ false
164
+ end
146
165
  rescue StandardError
147
166
  false
148
167
  end
@@ -22,13 +22,14 @@ module Docscribe
22
22
  # @param [Hash<Symbol, Object>] options parsed CLI options
23
23
  # @return [Docscribe::Config] merged effective config
24
24
  def build(base, options)
25
- return base unless needs_override?(options)
25
+ return warn_and_return_base(base, options) unless needs_override?(options)
26
26
 
27
27
  raw = Marshal.load(Marshal.dump(base.raw))
28
28
  apply_filter_overrides(raw, options)
29
29
  apply_rbs_overrides(raw, options) if rbs_overrides?(options)
30
30
  apply_sorbet_overrides(raw, options) if sorbet_overrides?(options)
31
31
  apply_output_overrides(raw, options)
32
+ apply_validation_overrides(raw, options) if validation_overrides?(options)
32
33
  conf = Docscribe::Config.new(config_path: base.config_path, **raw)
33
34
  warn_missing_rbs_collection(conf, options)
34
35
  conf
@@ -43,7 +44,21 @@ module Docscribe
43
44
  filter_overrides?(options) ||
44
45
  rbs_overrides?(options) ||
45
46
  sorbet_overrides?(options) ||
46
- output_overrides?(options)
47
+ output_overrides?(options) ||
48
+ validation_overrides?(options)
49
+ end
50
+
51
+ # Warn about a missing collection opt-in and return the base config unchanged.
52
+ #
53
+ # Used when no CLI override is present so the nudge still fires on plain runs.
54
+ #
55
+ # @note module_function: defines #warn_and_return_base (visibility: private)
56
+ # @param [Docscribe::Config] base base config loaded from YAML/defaults
57
+ # @param [Hash<Symbol, Object>] options parsed CLI options
58
+ # @return [Docscribe::Config] base config unchanged
59
+ def warn_and_return_base(base, options)
60
+ warn_missing_rbs_collection(base, options)
61
+ base
47
62
  end
48
63
 
49
64
  # Whether any method or file filter CLI options were provided.
@@ -195,6 +210,25 @@ module Docscribe
195
210
  raw['emit']['include_param_documentation'] = false if options[:no_boilerplate]
196
211
  end
197
212
 
213
+ # Whether any validation-related CLI options were provided.
214
+ #
215
+ # @note module_function: defines #validation_overrides? (visibility: private)
216
+ # @param [Hash<Symbol, Object>] options parsed CLI options
217
+ # @return [Boolean]
218
+ def validation_overrides?(options)
219
+ !options[:validate_types].nil?
220
+ end
221
+
222
+ # Apply validation-related CLI overrides to the raw config.
223
+ #
224
+ # @note module_function: defines #apply_validation_overrides (visibility: private)
225
+ # @param [Hash<String, Object>] raw raw config hash
226
+ # @param [Hash<Symbol, Object>] options parsed CLI options
227
+ # @return [void]
228
+ def apply_validation_overrides(raw, options)
229
+ raw['validate_types'] = options[:validate_types]
230
+ end
231
+
198
232
  # Warn when rbs_collection.lock.yaml exists but --rbs-collection was not passed.
199
233
  #
200
234
  # The warning can be suppressed by setting `rbs.warn_missing_collection: false`
@@ -206,6 +240,7 @@ module Docscribe
206
240
  # @return [void]
207
241
  def warn_missing_rbs_collection(conf, options)
208
242
  return if options[:rbs_collection]
243
+ return if conf.raw.dig('rbs', 'collection')
209
244
  return unless conf.rbs_warn_missing_collection?
210
245
  return unless File.exist?('rbs_collection.lock.yaml')
211
246
 
@@ -172,7 +172,7 @@ module Docscribe
172
172
  end
173
173
 
174
174
  # @private
175
- # @param [Object] node
175
+ # @param [Parser::AST::Node] node
176
176
  # @param [Docscribe::CLI::Coverage::CoverageStats] stats
177
177
  # @param [String] src
178
178
  # @return [void]
@@ -185,7 +185,7 @@ module Docscribe
185
185
  end
186
186
 
187
187
  # @private
188
- # @param [Object] node
188
+ # @param [Parser::AST::Node] node
189
189
  # @param [Docscribe::CLI::Coverage::CoverageStats] stats
190
190
  # @param [String] src
191
191
  # @return [void]
@@ -202,7 +202,7 @@ module Docscribe
202
202
  end
203
203
 
204
204
  # @private
205
- # @param [Object] node
205
+ # @param [Parser::AST::Node] node
206
206
  # @param [Docscribe::CLI::Coverage::CoverageStats] stats
207
207
  # @param [String] doc_comment
208
208
  # @return [void]
@@ -218,7 +218,7 @@ module Docscribe
218
218
  end
219
219
 
220
220
  # @private
221
- # @param [Object] node
221
+ # @param [Parser::AST::Node] node
222
222
  # @param [Docscribe::CLI::Coverage::CoverageStats] stats
223
223
  # @return [void]
224
224
  def count_method_params(node, stats)
@@ -226,7 +226,7 @@ module Docscribe
226
226
  end
227
227
 
228
228
  # @private
229
- # @param [Object] node
229
+ # @param [Parser::AST::Node] node
230
230
  # @return [Integer]
231
231
  def count_params(node)
232
232
  args_node = node.children[2] || node.children[1]