docscribe 1.5.1 → 1.6.0

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: 6bbceb4704c077ffbe69c69d769bc5790347bbe3c64fce843069638214e53ee6
4
- data.tar.gz: 22a7dd5cfa26ea46bac3e3a47e5e228cd3b0c6520fb3a3909dff09e66b1099ee
3
+ metadata.gz: 1016144165d119ade7719092f38974b29082a1bd86824864dd3a9b3412268771
4
+ data.tar.gz: ed2f6bbaf4e6f5892e0977a34c9aecb4924badc7e21ef25892d88bb4b0f8611a
5
5
  SHA512:
6
- metadata.gz: 576a4645d81448f7cbceb93584e62710d54885d12d099b601d02d50b014d64cec6747756e7ed647d3dda35d12f734646b20508ab9080793e3ae1c26d027da6ba
7
- data.tar.gz: f69b8cd81d990e88a292d0ada1eae224e521d9d6d6f4ced608a06cb71550bddbeb88723407332ea82b5c7c1c564ce6a6fd56286829d3ccb3077a7ed0ceb08629
6
+ metadata.gz: 7d1a11cef005ba87ef7fdf97d594ea699d638bc2a75dd62838ccec6f9260fca9b090e44a3176957584a30506f1159545354566a372c54e16382a2d384ae8248d
7
+ data.tar.gz: fd23695cdba7b7726e306ac2300665cb1f5eaf2764d2e19946df99fec6f7fee112408459fc774710305358a55d394413c4c8b3c663bd432514ffaf8f939d6dcf
data/README.md CHANGED
@@ -73,72 +73,74 @@ docscribe -A lib
73
73
 
74
74
  ## Contents
75
75
 
76
- * [Docscribe](#docscribe)
77
- * [Quick start](#quick-start)
78
- * [Contents](#contents)
79
- * [Installation](#installation)
80
- * [Architecture](#architecture)
81
- * [Data flow](#data-flow)
82
- * [CLI](#cli)
83
- * [Exit codes](#exit-codes)
84
- * [Options](#options)
85
- * [Examples](#examples)
86
- * [`docscribe sigs` — check RBS signature coverage](#docscribe-sigs--check-rbs-signature-coverage)
87
- * [`docscribe rbs` — generate RBS from YARD](#docscribe-rbs--generate-rbs-from-yard)
88
- * [`docscribe update_types` — two-pass type-aware documentation update](#docscribe-update_types--two-pass-type-aware-documentation-update)
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)
91
- * [Update strategies](#update-strategies)
92
- * [Safe strategy](#safe-strategy)
93
- * [Aggressive strategy](#aggressive-strategy)
94
- * [Output markers](#output-markers)
95
- * [Tips & tricks](#tips--tricks)
96
- * [Parser backend (Parser gem vs Prism)](#parser-backend-parser-gem-vs-prism)
97
- * [External type integrations (optional)](#external-type-integrations-optional)
98
- * [RBS](#rbs)
99
- * [RBS collection auto-discovery](#rbs-collection-auto-discovery)
100
- * [Sorbet](#sorbet)
101
- * [Inline Sorbet example](#inline-sorbet-example)
102
- * [Sorbet RBI example](#sorbet-rbi-example)
103
- * [Sorbet comment placement](#sorbet-comment-placement)
104
- * [Generic type formatting](#generic-type-formatting)
105
- * [Notes and fallback behavior](#notes-and-fallback-behavior)
106
- * [Type inference](#type-inference)
107
- * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
108
- * [Visibility semantics](#visibility-semantics)
109
- * [API (library) usage](#api-library-usage)
110
- * [Plugin system](#plugin-system)
111
- * [TagPlugin](#tagplugin)
112
- * [CollectorPlugin](#collectorplugin)
113
- * [Plugin doc normalization (CollectorPlugin)](#plugin-doc-normalization-collectorplugin)
114
- * [`method_override` (structured patch)](#method_override-structured-patch)
115
- * [Registering plugins](#registering-plugins)
116
- * [Plugin priority](#plugin-priority)
117
- * [Idempotency](#idempotency)
118
- * [Plugin examples](#plugin-examples)
119
- * [Configuration](#configuration)
120
- * [Anonymous block parameters](#anonymous-block-parameters)
121
- * [Filtering](#filtering)
122
- * [`attr_*` example](#attr_-example)
123
- * [`Struct.new` examples](#structnew-examples)
124
- * [Constant-assigned struct](#constant-assigned-struct)
125
- * [Class-based struct](#class-based-struct)
126
- * [Merge behavior](#merge-behavior)
127
- * [Param tag style](#param-tag-style)
128
- * [Create a starter config](#create-a-starter-config)
129
- * [Generate a plugin skeleton](#generate-a-plugin-skeleton)
130
- * [Full configuration reference](#full-configuration-reference)
131
- * [CI integration](#ci-integration)
132
- * [Comparison to YARD's parser](#comparison-to-yards-parser)
133
- * [Limitations](#limitations)
134
- * [Roadmap](#roadmap)
135
- * [Editor Integration](#editor-integration)
136
- * [VS Code](#vs-code)
137
- * [RubyMine](#rubymine)
138
- * [Contributing](#contributing)
139
- * [Discussion & Community](#discussion--community)
140
- * [Logo Attribution](#logo-attribution)
141
- * [License](#license)
76
+ * [Quick start](#quick-start)
77
+ * [Contents](#contents)
78
+ * [Installation](#installation)
79
+ * [Architecture](#architecture)
80
+ * [Data flow](#data-flow)
81
+ * [CLI](#cli)
82
+ * [Exit codes](#exit-codes)
83
+ * [Options](#options)
84
+ * [Examples](#examples)
85
+ * [`docscribe sigs` — check RBS signature coverage](#docscribe-sigs--check-rbs-signature-coverage)
86
+ * [`docscribe rbs` — generate RBS from YARD](#docscribe-rbs--generate-rbs-from-yard)
87
+ * [`docscribe update_types` — two-pass type-aware documentation update](#docscribe-update_types--two-pass-type-aware-documentation-update)
88
+ * [`docscribe check_for_comments` — find placeholder documentation](#docscribe-check_for_comments--find-placeholder-documentation)
89
+ * [`docscribe init` — project scaffolding](#docscribe-init--project-scaffolding)
90
+ * [`docscribe config` — view resolved configuration](#docscribe-config--view-resolved-configuration)
91
+ * [`docscribe coverage` — documentation coverage report](#docscribe-coverage--documentation-coverage-report)
92
+ * [`docscribe server` — persistent daemon mode](#docscribe-server--persistent-daemon-mode)
93
+ * [Update strategies](#update-strategies)
94
+ * [Safe strategy](#safe-strategy)
95
+ * [Aggressive strategy](#aggressive-strategy)
96
+ * [Output markers](#output-markers)
97
+ * [Tips & tricks](#tips--tricks)
98
+ * [Parser backend (Parser gem vs Prism)](#parser-backend-parser-gem-vs-prism)
99
+ * [External type integrations (optional)](#external-type-integrations-optional)
100
+ * [RBS](#rbs)
101
+ * [RBS collection auto-discovery](#rbs-collection-auto-discovery)
102
+ * [Sorbet](#sorbet)
103
+ * [Inline Sorbet example](#inline-sorbet-example)
104
+ * [Sorbet RBI example](#sorbet-rbi-example)
105
+ * [Sorbet comment placement](#sorbet-comment-placement)
106
+ * [Generic type formatting](#generic-type-formatting)
107
+ * [Notes and fallback behavior](#notes-and-fallback-behavior)
108
+ * [Type inference](#type-inference)
109
+ * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
110
+ * [Visibility semantics](#visibility-semantics)
111
+ * [API (library) usage](#api-library-usage)
112
+ * [Plugin system](#plugin-system)
113
+ * [TagPlugin](#tagplugin)
114
+ * [CollectorPlugin](#collectorplugin)
115
+ * [Plugin doc normalization (CollectorPlugin)](#plugin-doc-normalization-collectorplugin)
116
+ * [`method_override` (structured patch)](#method_override-structured-patch)
117
+ * [Registering plugins](#registering-plugins)
118
+ * [Plugin priority](#plugin-priority)
119
+ * [Idempotency](#idempotency)
120
+ * [Plugin examples](#plugin-examples)
121
+ * [Configuration](#configuration)
122
+ * [Anonymous block parameters](#anonymous-block-parameters)
123
+ * [Filtering](#filtering)
124
+ * [`attr_*` example](#attr_-example)
125
+ * [`Struct.new` examples](#structnew-examples)
126
+ * [Constant-assigned struct](#constant-assigned-struct)
127
+ * [Class-based struct](#class-based-struct)
128
+ * [Merge behavior](#merge-behavior)
129
+ * [Param tag style](#param-tag-style)
130
+ * [Create a starter config](#create-a-starter-config)
131
+ * [Generate a plugin skeleton](#generate-a-plugin-skeleton)
132
+ * [Full configuration reference](#full-configuration-reference)
133
+ * [CI integration](#ci-integration)
134
+ * [Comparison to YARD's parser](#comparison-to-yards-parser)
135
+ * [Limitations](#limitations)
136
+ * [Roadmap](#roadmap)
137
+ * [Editor Integration](#editor-integration)
138
+ * [VS Code](#vs-code)
139
+ * [RubyMine](#rubymine)
140
+ * [Contributing](#contributing)
141
+ * [Discussion & Community](#discussion--community)
142
+ * [Logo Attribution](#logo-attribution)
143
+ * [License](#license)
142
144
 
143
145
  ## Installation
144
146
 
@@ -411,6 +413,9 @@ If you pass no files and don't use `--stdin`, Docscribe processes the current di
411
413
  - `--progress`
412
414
  Show progress (`[N/total] path/to/file.rb`) on stderr as each file is processed.
413
415
 
416
+ - `DOCSCRIBE_THREADS` (env var)
417
+ Number of parallel worker threads (default: 4). Set to `1` for sequential processing.
418
+
414
419
  - `--quiet` (`-q`)
415
420
  Only show status, no details (suppresses change reasons).
416
421
  Overrides the default detailed output.
@@ -645,10 +650,68 @@ Exit code `0` if no placeholders found, `1` if any are detected.
645
650
 
646
651
  - `-h`, `--help` — show help.
647
652
 
653
+ ### `docscribe init` — project scaffolding
654
+
655
+ `docscribe init` creates a default `docscribe.yml` config file in the current directory. It asks interactive questions
656
+ about your project and writes a tailored configuration.
657
+
658
+ ```shell
659
+ # Create docscribe.yml interactively
660
+ docscribe init
661
+
662
+ # Create with default settings (non-interactive)
663
+ docscribe init --default
664
+ ```
665
+
666
+ **Flags:**
667
+
668
+ - `--pre-commit` — generate and install a Git pre-commit hook that checks documentation on staged `.rb` files using
669
+ `docscribe --verbose`. The hook exits with a warning if staged files lack documentation.
670
+ - `--pre-commit --uninstall` — remove the pre-commit hook.
671
+ - `--pre-commit --dry-run` — preview the hook script without installing.
672
+ - `--default` — skip interactive prompts, write default config.
673
+ - `-h`, `--help` — show help.
674
+
675
+ ### `docscribe config` — view resolved configuration
676
+
677
+ `docscribe config` prints the fully resolved configuration (defaults + file + CLI overrides) as YAML.
678
+
679
+ ```shell
680
+ # Print resolved config
681
+ docscribe config
682
+
683
+ # Use a specific config file
684
+ docscribe config --config path/to/docscribe.yml
685
+ ```
686
+
687
+ **Flags:**
688
+
689
+ - `--config PATH` — path to config file.
690
+ - `-h`, `--help` — show help.
691
+
692
+ ### `docscribe coverage` — documentation coverage report
693
+
694
+ `docscribe coverage` scans Ruby files and reports the percentage of documented methods, parameters, and return types.
695
+
696
+ ```shell
697
+ # Generate coverage report (text table)
698
+ docscribe coverage lib
699
+
700
+ # JSON output for tooling
701
+ docscribe coverage --format json lib
702
+ ```
703
+
704
+ **Flags:**
705
+
706
+ - `--format FORMAT` — output format: `text` (default, human-readable table) or `json` (machine-readable).
707
+ - `--config PATH` — path to config file.
708
+ - `-h`, `--help` — show help.
709
+
648
710
  ### `docscribe server` — persistent daemon mode
649
711
 
650
712
  > [!NOTE]
651
- > Server mode requires **Ruby 3.1+** (`Process.fork` for background daemon).
713
+ > Server mode requires **Ruby 3.1+** and a platform with `Process.fork` and Unix domain sockets.
714
+ > Not available on **Windows** (no Unix sockets) or **JRuby** (no `Process.fork`).
652
715
 
653
716
  `docscribe server` starts a background daemon that keeps the Ruby runtime loaded and caches parsed ASTs across
654
717
  invocations. Subsequent `docscribe` calls with `--server` communicate with the daemon over a Unix socket instead of
@@ -703,6 +766,25 @@ docscribe-client --ping
703
766
  docscribe-client --shutdown
704
767
  ```
705
768
 
769
+ Additional thin client commands:
770
+
771
+ ```shell
772
+ # Print the Unix socket path (for IDE plugin integration)
773
+ docscribe-client --socket-path
774
+
775
+ # Show server info: version, pid, uptime, config path
776
+ docscribe-client --info
777
+
778
+ # Auto-start the daemon before running a check or fix
779
+ docscribe-client --ensure --check lib/user.rb
780
+
781
+ # Start the daemon standalone (exits when ready)
782
+ docscribe-client --ensure
783
+
784
+ # Batch check multiple files in one RPC call
785
+ docscribe-client --check-batch lib/a.rb,lib/b.rb,lib/c.rb
786
+ ```
787
+
706
788
  The thin client is used automatically by
707
789
  the [VS Code](https://marketplace.visualstudio.com/items?itemName=unurgunite.docscribe-vscode)
708
790
  and [RubyMine](https://plugins.jetbrains.com/plugin/32349-docscribe) plugins when the daemon is running.
@@ -717,6 +799,41 @@ The daemon's socket path includes a hash of:
717
799
  If any of these files change, the next `docscribe --server` call will start a new daemon automatically. The old daemon
718
800
  is left to idle-timeout on its own.
719
801
 
802
+ **JSON-RPC protocol (semi-stable API):**
803
+
804
+ The daemon speaks JSON-RPC 2.0 over Unix socket. Each request is a JSON line, each response is a JSON line.
805
+
806
+ Methods:
807
+
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` |
815
+
816
+ Error codes (standardized for IDE plugin integration):
817
+
818
+ | Code | Meaning | `error.data` fields |
819
+ |----------|-------------------------------|-----------------------------|
820
+ | `-32000` | Gem/dependency not found | `gem` |
821
+ | `-32001` | Syntax error in analyzed file | `file`, `line`, `detail` |
822
+ | `-32002` | Config load failure | — |
823
+ | `-32010` | Timeout | `timeout`, `file` |
824
+ | `-32099` | Internal unhandled error | `backtrace` (first 5 lines) |
825
+
826
+ **Socket path derivation:**
827
+
828
+ The socket path is a deterministic hash of:
829
+ - Working directory (`Dir.pwd`)
830
+ - `Gemfile.lock` mtime and `rbs_collection.lock.yaml` mtime (environment invalidation)
831
+ - Config file path and mtime (when `--config` is used)
832
+
833
+ Result: `/tmp/docscribe-<MD5 hash>.sock`
834
+
835
+ IDE plugins should use `docscribe-client --socket-path` instead of reimplementing this algorithm.
836
+
720
837
  **CLI override handling:**
721
838
 
722
839
  When CLI overrides (`-C`, `--include`, `--exclude`, etc.) change between requests, the daemon resets its effective
@@ -743,6 +860,17 @@ Safe strategy:
743
860
 
744
861
  This is the recommended day-to-day mode.
745
862
 
863
+ **`@!attribute` merge policy in safe mode:**
864
+
865
+ When a method already has a YARD `@!attribute` doc block (e.g. `# @!attribute [rw] name`), safe mode does not
866
+ overwrite it. Instead it merges only the missing parts:
867
+
868
+ - If the attribute has `[r]` or `[rw]` access and no `@return` tag exists — adds `@return [Type]`.
869
+ - If the attribute has `[w]` or `[rw]` access and no `@param` tag exists — adds `@param value [Type]`.
870
+ - If some attrs from the `attr_*` call are present in the doc block but others are missing — adds only the missing ones.
871
+
872
+ This preserves hand-written annotations while ensuring complete documentation.
873
+
746
874
  ### Aggressive strategy
747
875
 
748
876
  Used by:
@@ -1874,16 +2002,10 @@ yard doc -o docs
1874
2002
 
1875
2003
  ## Roadmap
1876
2004
 
1877
- - Method behavior inference from AST;
2005
+ - Overload-aware signature selection;
1878
2006
  - Deeper YARD integration — parse `.c` source comments and generate docs for C-extensions;
1879
2007
  - Custom parser plugins — support non-Ruby languages (Crystal, etc.) via the plugin system;
1880
- - Effective config dump;
1881
- - Overload-aware signature selection;
1882
- - Manual `@!attribute` merge policy;
1883
- - Richer inference for common APIs;
1884
- - Documentation coverage report — percentage of documented methods, params, returns;
1885
- - Pre-commit hook auto-install (`docscribe init --pre-commit`);
1886
- - Parallel processing for large codebases.
2008
+ - Richer inference for common APIs.
1887
2009
 
1888
2010
  ## Editor Integration
1889
2011
 
data/exe/docscribe-client CHANGED
@@ -60,6 +60,9 @@ end
60
60
  config_path = nil
61
61
  mode = nil
62
62
  file = nil
63
+ ensure_flag = false
64
+ batch_files = nil
65
+ batch_timeout = nil
63
66
 
64
67
  OptionParser.new do |opts|
65
68
  opts.on('-C', '--config <path>', 'Config file path') { |v| config_path = v }
@@ -71,20 +74,65 @@ OptionParser.new do |opts|
71
74
  mode = :fix
72
75
  file = v
73
76
  end
77
+ opts.on('--check-batch <files>', 'Check multiple files (comma-separated)') do |v|
78
+ mode = :check_batch
79
+ batch_files = v
80
+ end
81
+ opts.on('--timeout <seconds>', 'Timeout per file in batch mode') { |v| batch_timeout = v.to_f }
74
82
  opts.on('--shutdown', 'Shutdown the server') { mode = :shutdown }
75
83
  opts.on('--status', 'Show server status') { mode = :status }
76
84
  opts.on('--ping', 'Ping the server') { mode = :ping }
85
+ opts.on('--socket-path', 'Print socket path and exit') { mode = :socket_path }
86
+ opts.on('--info', 'Show server info') { mode = :info }
87
+ opts.on('--ensure', 'Ensure server is running before check/fix') { ensure_flag = true }
77
88
  end.parse!
78
89
 
79
90
  sock = socket_path(config_path)
80
91
 
92
+ if ensure_flag && mode.nil?
93
+ require 'docscribe/server'
94
+ Docscribe::Server.ensure_running!(config_path: config_path)
95
+ exit 0
96
+ end
97
+
98
+ if ensure_flag && %i[check fix].include?(mode)
99
+ begin
100
+ UNIXSocket.new(sock).close
101
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
102
+ require 'docscribe/server'
103
+ Docscribe::Server.ensure_running!(config_path: config_path)
104
+ rescue StandardError => e
105
+ warn "docscribe: #{e.message}"
106
+ exit 1
107
+ end
108
+ end
109
+
81
110
  case mode
111
+ when :socket_path
112
+ puts sock
113
+ when :info
114
+ result = send_request(sock, 'ping')
115
+ if result && result['result']
116
+ info = result['result']
117
+ info['status'] = 'running'
118
+ info['config_path'] = config_path if config_path
119
+ info['cli_overrides'] = {}
120
+ puts JSON.generate(info)
121
+ else
122
+ puts JSON.generate({ status: 'not_running', socket_path: sock })
123
+ end
82
124
  when :check
83
125
  result = send_request(sock, 'check', file: file, strategy: :safe)
84
126
  puts JSON.generate(result || { error: 'Connection failed' })
85
127
  when :fix
86
128
  result = send_request(sock, 'fix', file: file, strategy: :safe)
87
129
  puts JSON.generate(result || { error: 'Connection failed' })
130
+ when :check_batch
131
+ files = batch_files.split(',').map(&:strip)
132
+ params = { files: files, strategy: :safe }
133
+ params[:timeout] = batch_timeout if batch_timeout
134
+ result = send_request(sock, 'check_batch', **params)
135
+ puts JSON.generate(result || { error: 'Connection failed' })
88
136
  when :shutdown
89
137
  result = send_request(sock, 'shutdown')
90
138
  puts JSON.generate(result || { error: 'Connection failed' })
@@ -100,6 +148,8 @@ when :status
100
148
  end
101
149
  puts JSON.generate(alive ? { status: 'running', socket: sock } : { status: 'not_running' })
102
150
  else
103
- warn "Usage: #{$PROGRAM_NAME} --check <file> | --fix <file> | --shutdown | --status | --ping"
151
+ warn "Usage: #{$PROGRAM_NAME} --check <file> | --fix <file> " \
152
+ '| --check-batch <files> | --shutdown | --status | --ping ' \
153
+ '| --socket-path | --info | --ensure'
104
154
  exit 1
105
155
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+ require 'docscribe/config'
6
+ require 'docscribe/cli/config_builder'
7
+
8
+ module Docscribe
9
+ module CLI
10
+ # Print the fully resolved configuration as YAML.
11
+ module ConfigDump
12
+ BANNER = <<~TEXT
13
+ Usage: docscribe config [options]
14
+
15
+ Print the fully resolved configuration as YAML.
16
+
17
+ Options:
18
+ TEXT
19
+
20
+ class << self
21
+ # @param [Array<String>] argv
22
+ # @return [Integer]
23
+ def run(argv)
24
+ opts = parse_options(argv)
25
+ return 0 if opts[:help]
26
+
27
+ conf = Docscribe::Config.load(opts[:config])
28
+ conf = Docscribe::CLI::ConfigBuilder.build(conf, parse_cli_overrides(argv))
29
+
30
+ puts conf.to_h.to_yaml
31
+ 0
32
+ end
33
+
34
+ private
35
+
36
+ # @private
37
+ # @param [Array<String>] argv
38
+ # @return [Hash<Symbol, Object>]
39
+ def parse_options(argv)
40
+ opts = { config: nil }
41
+ build_parser(opts).parse!(argv)
42
+ opts
43
+ end
44
+
45
+ # @private
46
+ # @param [Hash<Symbol, Object>] opts
47
+ # @return [OptionParser]
48
+ def build_parser(opts)
49
+ OptionParser.new do |o|
50
+ o.banner = BANNER
51
+ o.on('--config PATH', 'Path to config file') { |v| opts[:config] = v }
52
+ o.on('-h', '--help', 'Show help') do
53
+ opts[:help] = true
54
+ puts o
55
+ end
56
+ end
57
+ end
58
+
59
+ # @private
60
+ # @param [Array<String>] argv
61
+ # @return [Hash<Symbol, Object>]
62
+ def parse_cli_overrides(argv)
63
+ opts = Docscribe::CLI::Options.parse!(argv)
64
+ opts.slice(:rbs, :rbs_collection, :sorbet, :sig_dirs, :rbi_dirs, :include, :exclude, :include_file,
65
+ :exclude_file)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end