docscribe 1.5.0 → 1.5.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8183e1f4497ca5a00d13a9cc6457540db7b99ff85438be4bbd7eae2a26f4d785
4
- data.tar.gz: d28fd30bfe8a940d43a1c545fccb94a10de2f60dc46870c737fa05fbd0e593be
3
+ metadata.gz: 6bbceb4704c077ffbe69c69d769bc5790347bbe3c64fce843069638214e53ee6
4
+ data.tar.gz: 22a7dd5cfa26ea46bac3e3a47e5e228cd3b0c6520fb3a3909dff09e66b1099ee
5
5
  SHA512:
6
- metadata.gz: 86a886940d37aaab2b0a1f0d3393315a308a7d0f1ca6a1330bfc54977550a49d7b5763067e71a8b581fa97cb1223acfdd005ec3cab59a52e96049c114191f4fc
7
- data.tar.gz: fc1e633462359286fcb528d7f9b8e8db9f87a50ba4c131fbfd75cb3fb84ba62ec06bc9bfc00bee467e02a1ea5a9cf04733063aadcd7d2ac127298fb7e11ac452
6
+ metadata.gz: 576a4645d81448f7cbceb93584e62710d54885d12d099b601d02d50b014d64cec6747756e7ed647d3dda35d12f734646b20508ab9080793e3ae1c26d027da6ba
7
+ data.tar.gz: f69b8cd81d990e88a292d0ada1eae224e521d9d6d6f4ced608a06cb71550bddbeb88723407332ea82b5c7c1c564ce6a6fd56286829d3ccb3077a7ed0ceb08629
data/README.md CHANGED
@@ -1,15 +1,17 @@
1
- # Docscribe
1
+ <p align="center">
2
+ <img src="assets/icons/icon_128x128.png" alt="Docscribe logo" width="128">
3
+ </p>
2
4
 
3
- [![Gem Version](https://img.shields.io/gem/v/docscribe.svg)](https://rubygems.org/gems/docscribe)
4
- [![RubyGems Downloads](https://img.shields.io/gem/dt/docscribe.svg)](https://rubygems.org/gems/docscribe)
5
- [![CI](https://github.com/unurgunite/docscribe/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/unurgunite/docscribe/actions/workflows/ci.yml)
6
- [![License](https://img.shields.io/github/license/unurgunite/docscribe.svg)](https://github.com/unurgunite/docscribe/blob/master/LICENSE.txt)
7
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-blue.svg)](#installation)
8
- [![VS Code](https://img.shields.io/badge/VS%20Code-plugin-blue?logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=unurgunite.docscribe-vscode)
9
- [![RubyMine](https://img.shields.io/badge/RubyMine-plugin-green?logo=jetbrains)](https://plugins.jetbrains.com/plugin/32349-docscribe)
5
+ <h1 align="center">DocScribe</h1>
10
6
 
11
- <p>
12
- <img src="assets/icons/icon_256x256.png" alt="Docscribe logo" width="96">
7
+ <p align="center">
8
+ <a href="https://rubygems.org/gems/docscribe"><img src="https://img.shields.io/gem/v/docscribe.svg" alt="Gem Version"></a>
9
+ <a href="https://rubygems.org/gems/docscribe"><img src="https://img.shields.io/gem/dt/docscribe.svg" alt="RubyGems Downloads"></a>
10
+ <a href="https://github.com/unurgunite/docscribe/actions/workflows/ci.yml"><img src="https://github.com/unurgunite/docscribe/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
11
+ <a href="https://github.com/unurgunite/docscribe/blob/master/LICENSE.txt"><img src="https://img.shields.io/github/license/unurgunite/docscribe.svg" alt="License"></a>
12
+ <a href="#installation"><img src="https://img.shields.io/badge/ruby-%3E%3D%202.7-blue.svg" alt="Ruby"></a>
13
+ <a href="https://marketplace.visualstudio.com/items?itemName=unurgunite.docscribe-vscode"><img src="https://img.shields.io/badge/VS%20Code-plugin-blue?logo=visualstudiocode" alt="VS Code"></a>
14
+ <a href="https://plugins.jetbrains.com/plugin/32349-docscribe"><img src="https://img.shields.io/badge/RubyMine-plugin-green?logo=jetbrains" alt="RubyMine"></a>
13
15
  </p>
14
16
 
15
17
  ![Docscribe before/after demo](docs/image.png)
@@ -85,6 +87,7 @@ docscribe -A lib
85
87
  * [`docscribe rbs` — generate RBS from YARD](#docscribe-rbs--generate-rbs-from-yard)
86
88
  * [`docscribe update_types` — two-pass type-aware documentation update](#docscribe-update_types--two-pass-type-aware-documentation-update)
87
89
  * [`docscribe check_for_comments` — find placeholder documentation](#docscribe-check_for_comments--find-placeholder-documentation)
90
+ * [`docscribe server` — persistent daemon mode](#docscribe-server--persistent-daemon-mode)
88
91
  * [Update strategies](#update-strategies)
89
92
  * [Safe strategy](#safe-strategy)
90
93
  * [Aggressive strategy](#aggressive-strategy)
@@ -166,6 +169,10 @@ loading, then delegates to the core engine which parses source code, collects me
166
169
  doc lines — combining heuristic type inference, external RBS/Sorbet signatures, and plugin output — and finally rewrites
167
170
  the source via a strategy (safe merge or aggressive replace).
168
171
 
172
+ In server mode, a persistent daemon (`docscribe server`) keeps the runtime loaded and caches parsed results across
173
+ invocations via an LRU cache, enabling near-instant repeated checks for IDE plugins. A thin client (`docscribe-client`)
174
+ provides minimal-overhead socket communication without loading the full gem.
175
+
169
176
  ```mermaid
170
177
  flowchart TB
171
178
  subgraph CLI["CLI Layer"]
@@ -240,7 +247,18 @@ flowchart TB
240
247
  YTypes["Yard::Types\n9 AST node types\n(Named, Generic, etc.)"]
241
248
  end
242
249
 
250
+ subgraph Server["Server / Daemon"]
251
+ ThinClient["exe/docscribe-client\nThin client\n· socket send/receive\n· no gem load"]
252
+ ServerDaemon["Server::Daemon\nSocket listener\n· check / fix / shutdown\n· JSON-RPC 2.0"]
253
+ Cache["Docscribe::LRUCache\nFile result cache\n(max 1000, by mtime)"]
254
+ end
255
+
243
256
  Exe --> Run
257
+ Exe --> ServerDaemon
258
+ ThinClient --> ServerDaemon
259
+ ServerDaemon --> ConfigClass
260
+ ServerDaemon --> Cache
261
+ ServerDaemon --> InlineRewriter
244
262
  Run --> Options
245
263
  Run --> InitCmd
246
264
  Run --> GenCmd
@@ -249,6 +267,7 @@ flowchart TB
249
267
  Run --> SarifFormatter
250
268
  Run --> ConfigBuilder
251
269
  ConfigBuilder --> ConfigClass
270
+ ConfigBuilder --> ServerDaemon
252
271
  ConfigClass --> Defaults
253
272
  ConfigClass --> Loader
254
273
  ConfigClass --> Emit
@@ -298,7 +317,25 @@ flowchart TB
298
317
 
299
318
  ```mermaid
300
319
  flowchart LR
301
- Input["Source files\n(.rb)"] --> Parse["Parsing.parse_buffer\nParser gem / Prism"]
320
+ subgraph Entry["Entry Points"]
321
+ Direct["docscribe lib\n(no --server)"]
322
+ ViaServer["docscribe --server\nor docscribe-client"]
323
+ end
324
+
325
+ subgraph Daemon["Server Daemon (Unix Socket)"]
326
+ Socket["Daemon#listen_loop\nJSON-RPC 2.0 dispatch"]
327
+ CacheCheck{"File cached &\nmtime fresh?"}
328
+ CacheStorage["LRUCache\n(1000 entries)"]
329
+ ApplyOverrides["apply_cli_overrides\n(reset on nil)"]
330
+ end
331
+
332
+ ViaServer --> Socket
333
+ Socket --> ApplyOverrides
334
+ ApplyOverrides --> CacheCheck
335
+ CacheCheck -->|Hit| Socket
336
+ CacheCheck -->|Miss| Parse
337
+ Direct --> Parse
338
+ Parse["Parsing.parse_buffer\nParser gem / Prism"]
302
339
  Parse --> AST["AST + Comments"]
303
340
  AST --> Collect["Collector.process\n· Find methods\n· Track visibility\n· Find attr_*"]
304
341
  AST --> CollectPlugins["CollectorPlugin#collect\n· Custom AST walks\n· Non-standard constructs"]
@@ -317,7 +354,11 @@ flowchart LR
317
354
  Strategy -->|Aggressive| Replace["Replace entirely"]
318
355
  Merge --> Rewritten["Rewriter#process\n-> rewritten source"]
319
356
  Replace --> Rewritten
320
- Rewritten --> Output["Modified .rb file\n/ STDOUT"]
357
+ Rewritten --> Result["Result / response"]
358
+ Rewritten --> CacheStorage
359
+ CacheStorage --> Socket
360
+ Result -->|Direct mode| Output["Modified .rb file / STDOUT"]
361
+ Result -->|Server mode| Socket
321
362
  ```
322
363
 
323
364
  ## CLI
@@ -330,6 +371,7 @@ docscribe sigs [options] [files...]
330
371
  docscribe rbs [options] [files...]
331
372
  docscribe update_types [directory]
332
373
  docscribe check_for_comments [paths...]
374
+ docscribe server [start|status|stop] [options]
333
375
  ```
334
376
 
335
377
  Docscribe has three main ways to run:
@@ -376,6 +418,10 @@ If you pass no files and don't use `--stdin`, Docscribe processes the current di
376
418
  - `--explain`
377
419
  Show detailed reasons for each file (default; no-op for compatibility).
378
420
 
421
+ - `--server`
422
+ Run via a persistent daemon (Unix socket). Speeds up repeated invocations
423
+ by keeping the Ruby runtime loaded and caching results.
424
+
379
425
  - `-k`, `--keep-descriptions`
380
426
  Preserve existing documentation text when rebuilding doc blocks in aggressive mode.
381
427
 
@@ -599,6 +645,84 @@ Exit code `0` if no placeholders found, `1` if any are detected.
599
645
 
600
646
  - `-h`, `--help` — show help.
601
647
 
648
+ ### `docscribe server` — persistent daemon mode
649
+
650
+ > [!NOTE]
651
+ > Server mode requires **Ruby 3.1+** (`Process.fork` for background daemon).
652
+
653
+ `docscribe server` starts a background daemon that keeps the Ruby runtime loaded and caches parsed ASTs across
654
+ invocations. Subsequent `docscribe` calls with `--server` communicate with the daemon over a Unix socket instead of
655
+ reloading the entire toolchain.
656
+
657
+ ```shell
658
+ # Start the daemon (auto-detached in background)
659
+ docscribe server start
660
+
661
+ # Check if the daemon is running
662
+ docscribe server status
663
+
664
+ # Stop the daemon
665
+ docscribe server stop
666
+
667
+ # Use the daemon for file checks (much faster on repeated calls)
668
+ docscribe --server lib
669
+ docscribe -a --server lib
670
+ ```
671
+
672
+ **How it works:**
673
+
674
+ 1. `docscribe server start` forks a background process that listens on a Unix socket in the system temp directory.
675
+ 2. The socket path is derived from the project root, `Gemfile.lock` mtime, and `rbs_collection.lock.yaml` mtime — so any
676
+ environment change spawns a fresh daemon automatically.
677
+ 3. Client requests (`docscribe --server`) send JSON-RPC 2.0 messages over the socket: `check` (inspect), `fix` (apply
678
+ changes), `shutdown`, and `ping` (health/version info).
679
+ 4. The daemon holds a reusable `Docscribe::Config` instance and an LRU file cache (bounded at 1000 entries), so repeated
680
+ checks on the same files are nearly instant.
681
+ 5. After 5 minutes of inactivity the daemon shuts down automatically.
682
+
683
+ **Thin client (`docscribe-client`):**
684
+
685
+ For IDE plugins and CI systems that need minimal startup overhead, a standalone thin client is available as
686
+ `exe/docscribe-client`. It connects to the daemon via the same Unix socket protocol without loading the full docscribe
687
+ gem:
688
+
689
+ ```shell
690
+ # Check a file via the daemon
691
+ docscribe-client --check lib/user.rb
692
+
693
+ # Apply fixes via the daemon
694
+ docscribe-client --fix lib/user.rb
695
+
696
+ # Check if the daemon is running
697
+ docscribe-client --status
698
+
699
+ # Get version, pid, uptime from the daemon
700
+ docscribe-client --ping
701
+
702
+ # Stop the daemon
703
+ docscribe-client --shutdown
704
+ ```
705
+
706
+ The thin client is used automatically by
707
+ the [VS Code](https://marketplace.visualstudio.com/items?itemName=unurgunite.docscribe-vscode)
708
+ and [RubyMine](https://plugins.jetbrains.com/plugin/32349-docscribe) plugins when the daemon is running.
709
+
710
+ **Environment invalidation:**
711
+
712
+ The daemon's socket path includes a hash of:
713
+
714
+ - `Gemfile.lock` mtime — gem changes that may affect RBS signatures.
715
+ - `rbs_collection.lock.yaml` mtime — RBS collection signature updates.
716
+
717
+ If any of these files change, the next `docscribe --server` call will start a new daemon automatically. The old daemon
718
+ is left to idle-timeout on its own.
719
+
720
+ **CLI override handling:**
721
+
722
+ When CLI overrides (`-C`, `--include`, `--exclude`, etc.) change between requests, the daemon resets its effective
723
+ configuration, clears the file cache, and applies the new overrides before processing. If no overrides are passed, the
724
+ cached state from the initial load is used, preserving performance.
725
+
602
726
  ## Update strategies
603
727
 
604
728
  Docscribe supports two update strategies: **safe** and **aggressive**.
@@ -1757,7 +1881,6 @@ yard doc -o docs
1757
1881
  - Overload-aware signature selection;
1758
1882
  - Manual `@!attribute` merge policy;
1759
1883
  - Richer inference for common APIs;
1760
- - Editor integration (LSP, VS Code extension);
1761
1884
  - Documentation coverage report — percentage of documented methods, params, returns;
1762
1885
  - Pre-commit hook auto-install (`docscribe init --pre-commit`);
1763
1886
  - Parallel processing for large codebases.
@@ -1784,6 +1907,10 @@ Docscribe provides IDE plugins for a better development experience:
1784
1907
 
1785
1908
  > [!NOTE]
1786
1909
  > Both plugins require **docscribe >= 1.5.0**.
1910
+ >
1911
+ > For optimal performance, both plugins use the `docscribe-client` thin client
1912
+ > to communicate with the docscribe daemon (see [server mode](#docscribe-server--persistent-daemon-mode)).
1913
+ > Start the daemon with `docscribe server start` for near-instant diagnostics.
1787
1914
 
1788
1915
  ## Contributing
1789
1916
 
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'socket'
6
+ require 'json'
7
+ require 'securerandom'
8
+ require 'digest/md5'
9
+ require 'tmpdir'
10
+
11
+ SOCKET_DIR = begin
12
+ tmp = Dir.tmpdir || '/tmp'
13
+ sock_overhead = "/docscribe-#{'a' * 32}.sock".bytesize
14
+ tmp.bytesize <= 104 - sock_overhead ? tmp : '/tmp'
15
+ end
16
+ ENV_FILES = %w[Gemfile.lock rbs_collection.lock.yaml].freeze
17
+
18
+ def socket_path(config_path = nil)
19
+ seed = +Dir.pwd
20
+ seed << ":#{env_hash}"
21
+ if config_path
22
+ resolved = File.expand_path(config_path)
23
+ mtime = File.exist?(resolved) ? File.mtime(resolved).to_f : 0.0
24
+ seed << ":#{resolved}:#{mtime}"
25
+ end
26
+ "#{SOCKET_DIR}/docscribe-#{Digest::MD5.hexdigest(seed)}.sock"
27
+ end
28
+
29
+ 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
34
+ Digest::MD5.hexdigest(parts.join(':'))
35
+ end
36
+
37
+ def config_hash(config_path)
38
+ resolved = File.expand_path(config_path)
39
+ mtime = File.exist?(resolved) ? File.mtime(resolved).to_f : 0.0
40
+ Digest::MD5.hexdigest("#{resolved}:#{mtime}")
41
+ end
42
+
43
+ def send_request(sock, method, params = {})
44
+ socket = UNIXSocket.new(sock)
45
+ req = build_request(method, params)
46
+ socket.write("#{JSON.generate(req)}\n")
47
+ socket.close_write
48
+ line = socket.gets
49
+ line ? JSON.parse(line) : nil
50
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
51
+ nil
52
+ ensure
53
+ socket&.close
54
+ end
55
+
56
+ def build_request(method, params = {})
57
+ { jsonrpc: '2.0', id: SecureRandom.uuid, method: method, params: params }
58
+ end
59
+
60
+ config_path = nil
61
+ mode = nil
62
+ file = nil
63
+
64
+ OptionParser.new do |opts|
65
+ opts.on('-C', '--config <path>', 'Config file path') { |v| config_path = v }
66
+ opts.on('--check <file>', 'Check a file') do |v|
67
+ mode = :check
68
+ file = v
69
+ end
70
+ opts.on('--fix <file>', 'Fix a file') do |v|
71
+ mode = :fix
72
+ file = v
73
+ end
74
+ opts.on('--shutdown', 'Shutdown the server') { mode = :shutdown }
75
+ opts.on('--status', 'Show server status') { mode = :status }
76
+ opts.on('--ping', 'Ping the server') { mode = :ping }
77
+ end.parse!
78
+
79
+ sock = socket_path(config_path)
80
+
81
+ case mode
82
+ when :check
83
+ result = send_request(sock, 'check', file: file, strategy: :safe)
84
+ puts JSON.generate(result || { error: 'Connection failed' })
85
+ when :fix
86
+ result = send_request(sock, 'fix', file: file, strategy: :safe)
87
+ puts JSON.generate(result || { error: 'Connection failed' })
88
+ when :shutdown
89
+ result = send_request(sock, 'shutdown')
90
+ puts JSON.generate(result || { error: 'Connection failed' })
91
+ when :ping
92
+ result = send_request(sock, 'ping')
93
+ puts JSON.generate(result || { error: 'Connection failed' })
94
+ when :status
95
+ alive = begin
96
+ File.exist?(sock) &&
97
+ UNIXSocket.new(sock).close && true
98
+ rescue StandardError
99
+ false
100
+ end
101
+ puts JSON.generate(alive ? { status: 'running', socket: sock } : { status: 'not_running' })
102
+ else
103
+ warn "Usage: #{$PROGRAM_NAME} --check <file> | --fix <file> | --shutdown | --status | --ping"
104
+ exit 1
105
+ end
@@ -29,7 +29,7 @@ module Docscribe
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
- conf = Docscribe::Config.new(raw)
32
+ conf = Docscribe::Config.new(config_path: base.config_path, **raw)
33
33
  warn_missing_rbs_collection(conf, options)
34
34
  conf
35
35
  end
@@ -12,6 +12,16 @@ module Docscribe
12
12
  # docscribe generate tag MyPlugin --output lib/docscribe_plugins
13
13
  # docscribe generate tag MyPlugin --stdout
14
14
  module Generate
15
+ BANNER = <<~TEXT
16
+ Usage: docscribe generate <type> <PluginName> [options]
17
+
18
+ Types:
19
+ tag Generate a TagPlugin skeleton
20
+ collector Generate a CollectorPlugin skeleton
21
+
22
+ Options:
23
+ TEXT
24
+
15
25
  PLUGIN_TYPES = %w[tag collector].freeze
16
26
 
17
27
  NEXT_STEPS_TEMPLATE = <<~TEXT
@@ -275,29 +285,13 @@ module Docscribe
275
285
  # @return [OptionParser]
276
286
  def build_option_parser(opts)
277
287
  OptionParser.new do |opt|
278
- opt.banner = parser_banner
288
+ opt.banner = BANNER
279
289
  register_output_option(opt, opts)
280
290
  register_stdout_option(opt, opts)
281
291
  register_help_option(opt, opts)
282
292
  end
283
293
  end
284
294
 
285
- # Return the usage banner for the generate subcommand parser.
286
- #
287
- # @private
288
- # @return [String]
289
- def parser_banner
290
- <<~TEXT
291
- Usage: docscribe generate <type> <PluginName> [options]
292
-
293
- Types:
294
- tag Generate a TagPlugin skeleton
295
- collector Generate a CollectorPlugin skeleton
296
-
297
- Options:
298
- TEXT
299
- end
300
-
301
295
  # Register the --output option on the OptionParser.
302
296
  #
303
297
  # @private
@@ -26,7 +26,8 @@ module Docscribe
26
26
  rbs_collection: false,
27
27
  keep_descriptions: false,
28
28
  no_boilerplate: false,
29
- progress: false
29
+ progress: false,
30
+ server: false
30
31
  }.freeze
31
32
 
32
33
  module_function
@@ -52,7 +53,8 @@ module Docscribe
52
53
 
53
54
  Input / config:
54
55
  --stdin Read code from STDIN and print rewritten output
55
- -C, --config PATH Path to config YAML (default: docscribe.yml)
56
+ -C, --config PATH Path to config YAML (default: docscribe.yml)
57
+ --server Use background server for faster repeated runs
56
58
 
57
59
  Type information:
58
60
  --rbs Use RBS signatures for @param/@return when available
@@ -146,6 +148,7 @@ module Docscribe
146
148
  def define_input_options(opts, options)
147
149
  define_stdin_option(opts, options)
148
150
  define_config_option(opts, options)
151
+ define_server_option(opts, options)
149
152
  end
150
153
 
151
154
  # Define stdin option
@@ -172,6 +175,18 @@ module Docscribe
172
175
  end
173
176
  end
174
177
 
178
+ # Define server option
179
+ #
180
+ # @note module_function: defines #define_server_option (visibility: private)
181
+ # @param [OptionParser] opts the option parser to configure
182
+ # @param [Hash<Symbol, Object>] options mutable parsed options hash
183
+ # @return [void]
184
+ def define_server_option(opts, options)
185
+ opts.on('--server', 'Use background server for faster repeated runs') do
186
+ options[:server] = true
187
+ end
188
+ end
189
+
175
190
  # Define type options
176
191
  #
177
192
  # @note module_function: defines #define_type_options (visibility: private)
@@ -162,7 +162,7 @@ module Docscribe
162
162
  # @param [Hash<Symbol, Object>] options
163
163
  # @raise [Parser::SyntaxError]
164
164
  # @raise [StandardError]
165
- # @return [Boolean] if StandardError
165
+ # @return [Boolean]
166
166
  # @return [Boolean] if Parser::SyntaxError
167
167
  # @return [Boolean] if StandardError
168
168
  def generate_for_file(path, options)