decode 0.25.0 → 0.27.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: 01244e4f44a9916295cace389ef8ad2116277a1e5831f500d1e13d953290386a
4
- data.tar.gz: e3a85a7b3c2c4d98aa8acc50026a2b515bc33db95fff2ec82c117cda0c1ded52
3
+ metadata.gz: 65f2a545a291a4258d019ea79ea9c4cda2e4e15af778670b505ac7652d620ee3
4
+ data.tar.gz: fa7d2328a1eba99917c23de03569575642ae587dbc049ab4a409d84e85824c57
5
5
  SHA512:
6
- metadata.gz: 974b45b31551d81b281fe5a483a93e0b3ed07a3b6508dbd30df014b45cc2b47a51a34ae6f20454bd973ec7fe5c23393a7176c20028e6dba9a1402fb987c79eb2
7
- data.tar.gz: 5028c457dc3c24d79996170725604fddffc127f96a96f8fdf5c3c4188162097538cb103be51808d125e3ed3edec786104874123197bd6e37bd8e463fcf7319b5
6
+ metadata.gz: 78ac2928c18dc95f78aa181cadca912776e288781ef3f88c0fbd507faae38884d2d66dcf73770dafeb874aff4ef69a2ecec601b6b3e097be02154fe7a06f187b
7
+ data.tar.gz: acef5f32fcb5b0ff49c076e706555c6eb2fbceed83abc9bd0f35557e72153fcde17fcd6cc31320eb9aee1c71fd73739554d757517b7105f8d080eee8ad71a0dc
checksums.yaml.gz.sig CHANGED
@@ -1 +1 @@
1
- 7��w1y��.��\��4Jԝ�4�%[kihL!j�4@g\�JͨȨ�V$N�&�XBdK#���u���Noz
1
+ Y��ʇ�7MI��t�s�5�/����EB�� R�t�� HX��D�bR��a��P������“�[��G�A~H����k��ZEk���!�$D����uJC��wH�k�y��p�_T�#�5G+�e_T���ԍ�uT�z�N��xZ`{y�~��25�G&�r�F�Jh�g�0��P���=6���5��l����zV�d�޼Y���&��ft��s�#?��� d�.�Lև���qD�8<=0F!T`Q�a��\���/L�pM� ��Yד*&�9��b� NԜ����RH��+ ߸#o�,5a�ކ.YN��_!���UPxM@y��N?!}3�Fz�펋��I/u��)��"6������S�@�pu��x�`1��N�
@@ -0,0 +1,378 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "fileutils"
7
+
8
+ def initialize(...)
9
+ super
10
+
11
+ require "decode/index"
12
+ require "yaml"
13
+ end
14
+
15
+ # Generate Markdown documentation for LLM consumption.
16
+ # @parameter root [String] The root path to index (e.g., 'lib').
17
+ # @parameter output_root [String] The root output directory (e.g., 'context').
18
+ # @parameter name [String] The subdirectory name for the generated files (e.g., 'interface').
19
+ def markdown(root, output_root: "context", name: "interface")
20
+ index = Decode::Index.for(root)
21
+
22
+ # Construct full output directory path
23
+ output_directory = File.join(output_root, name)
24
+
25
+ # Track all generated files for index.yaml
26
+ generated_files = []
27
+
28
+ # Group definitions by container (class/module)
29
+ containers = {}
30
+
31
+ # First pass: collect all definitions
32
+ index.definitions.each do |qualified_name, definition|
33
+ # Skip non-public definitions
34
+ next unless definition.public?
35
+
36
+ # If this is a container, register it
37
+ if definition.container?
38
+ containers[qualified_name] ||= {
39
+ definition: definition,
40
+ methods: [],
41
+ aliases: []
42
+ }
43
+ else
44
+ # This is a method/attribute - add to parent container
45
+ if parent = definition.parent
46
+ # Find the containing class/module
47
+ container_definition = parent
48
+ while container_definition && !container_definition.container?
49
+ container_definition = container_definition.parent
50
+ end
51
+
52
+ if container_definition
53
+ container_name = container_definition.qualified_name
54
+ containers[container_name] ||= {
55
+ definition: container_definition,
56
+ methods: [],
57
+ aliases: []
58
+ }
59
+ if definition.respond_to?(:alias?) && definition.alias?
60
+ containers[container_name][:aliases] << definition
61
+ else
62
+ containers[container_name][:methods] << definition
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ $stderr.puts "Found #{containers.size} containers to document"
70
+
71
+ # Generate markdown files for each container
72
+ containers.each do |qualified_name, data|
73
+ container = data[:definition]
74
+ # Preserve original code order as collected by the parser/index:
75
+ methods = data[:methods]
76
+ aliases = data[:aliases]
77
+
78
+ # Generate file path
79
+ file_path = File.join(output_directory, "#{qualified_name.gsub('::', '/')}.md")
80
+ FileUtils.mkdir_p(File.dirname(file_path))
81
+
82
+ # Generate markdown content
83
+ content = generate_container_markdown(container, methods, aliases)
84
+
85
+ File.write(file_path, content)
86
+ generated_files << {
87
+ path: file_path,
88
+ qualified_name: qualified_name,
89
+ kind: container.respond_to?(:container?) && container.container? ? "class/module" : "class/module"
90
+ }
91
+
92
+ $stderr.puts "Generated: #{file_path}"
93
+ end
94
+
95
+ $stderr.puts "Generated #{generated_files.size} files in #{output_directory}"
96
+
97
+ # Generate overview/index file
98
+ overview_path = File.join(output_root, "#{name}.md")
99
+ overview_content = generate_overview(name, containers, index)
100
+ File.write(overview_path, overview_content)
101
+ $stderr.puts "Generated overview: #{overview_path}"
102
+ end
103
+
104
+ private
105
+
106
+ # Generate markdown content for a container (class/module) and its methods.
107
+ # @parameter container [Decode::Definition]
108
+ # @parameter methods [Array]
109
+ # @parameter aliases [Array[Decode::Language::Ruby::Alias]]
110
+ def generate_container_markdown(container, methods, aliases)
111
+ lines = []
112
+
113
+ # Title
114
+ lines << "# #{container.qualified_name}"
115
+ lines << ""
116
+
117
+ # Summary from documentation
118
+ if documentation = container.documentation
119
+ if summary = extract_summary(documentation)
120
+ lines << summary
121
+ lines << ""
122
+ end
123
+ end
124
+
125
+ # Metadata
126
+ kind = case container
127
+ when Decode::Language::Ruby::Class
128
+ "Class"
129
+ when Decode::Language::Ruby::Module
130
+ "Module"
131
+ when Decode::Language::Ruby::Singleton
132
+ "Singleton"
133
+ else
134
+ "Container"
135
+ end
136
+
137
+ meta_lines = ["- Kind: #{kind}"]
138
+ if container.respond_to?(:super_class) && container.super_class
139
+ meta_lines << "- Superclass: #{container.super_class}"
140
+ end
141
+ if container.respond_to?(:includes) && container.includes.any?
142
+ meta_lines << "- Includes: #{container.includes.join(', ')}"
143
+ end
144
+ if container.respond_to?(:extends) && container.extends.any?
145
+ meta_lines << "- Extends: #{container.extends.join(', ')}"
146
+ end
147
+ if container.respond_to?(:prepends) && container.prepends.any?
148
+ meta_lines << "- Prepends: #{container.prepends.join(', ')}"
149
+ end
150
+ if container.parent
151
+ meta_lines << "- Namespace: #{container.parent.qualified_name}"
152
+ end
153
+
154
+ if meta_lines.any?
155
+ lines << "## Metadata"
156
+ lines << ""
157
+ lines.concat(meta_lines)
158
+ lines << ""
159
+ end
160
+
161
+ # Description
162
+ if documentation = container.documentation
163
+ if description = extract_description(documentation)
164
+ lines << "## Overview"
165
+ lines << ""
166
+ lines << description
167
+ lines << ""
168
+ end
169
+ end # Attributes
170
+ attributes = methods.select{|m| m.is_a?(Decode::Language::Ruby::Attribute) rescue false}
171
+ if attributes.any?
172
+ lines << "## Attributes"
173
+ lines << ""
174
+ attributes.each do |attribute|
175
+ lines.concat(generate_method_section(attribute))
176
+ end
177
+ end
178
+
179
+ # Methods
180
+ non_attributes = methods.reject{|m| m.is_a?(Decode::Language::Ruby::Attribute) rescue false}
181
+ if non_attributes.any?
182
+ lines << "## Methods"
183
+ lines << ""
184
+ non_attributes.each do |method|
185
+ lines.concat(generate_method_section(method, aliases))
186
+ end
187
+ end
188
+
189
+ lines.join("\n")
190
+ end
191
+
192
+ # Generate markdown for a single method.
193
+ # Also annotates any alias names that refer to this method within the same container.
194
+ def generate_method_section(method, aliases = [])
195
+ lines = []
196
+
197
+ # Method heading
198
+ lines << "### `#{method.nested_name}`"
199
+ lines << ""
200
+
201
+ # Also known as (aliases pointing to this method)
202
+ if aliases && !aliases.empty?
203
+ alias_names = aliases.select{|a| a.old_name == method.name}.map(&:name)
204
+ if alias_names.any?
205
+ lines << "_Also known as:_ #{alias_names.map{|n| "`#{n}`"}.join(", ")}"
206
+ lines << ""
207
+ end
208
+ end
209
+
210
+ # Summary
211
+ if documentation = method.documentation
212
+ if summary = extract_summary(documentation)
213
+ lines << summary
214
+ lines << ""
215
+ end
216
+ end
217
+
218
+ # Signature
219
+ if signature = method.long_form
220
+ lines << "**Signature:**"
221
+ lines << "```ruby"
222
+ lines << signature
223
+ lines << "```"
224
+ lines << ""
225
+ end
226
+
227
+ # Parameters
228
+ if documentation = method.documentation
229
+ parameters = documentation.filter(Decode::Comment::Parameter).to_a
230
+ if parameters.any?
231
+ lines << "**Parameters:**"
232
+ parameters.each do |parameter|
233
+ parameter_text = "- `#{parameter.name}` `#{parameter.type}`"
234
+ if description = parameter.text&.join(" ")
235
+ parameter_text << " — #{description}"
236
+ end
237
+ lines << parameter_text
238
+ end
239
+ lines << ""
240
+ end
241
+
242
+ # Returns
243
+ returns = documentation.filter(Decode::Comment::Returns).to_a
244
+ if returns.any?
245
+ lines << "**Returns:**"
246
+ returns.each do |return_tag|
247
+ return_text = "- `#{return_tag.type}`"
248
+ if description = return_tag.text&.join(" ")
249
+ return_text << " — #{description}"
250
+ end
251
+ lines << return_text
252
+ end
253
+ lines << ""
254
+ end
255
+
256
+ # Yields
257
+ yields_tags = documentation.filter(Decode::Comment::Yields).to_a
258
+ if yields_tags.any?
259
+ lines << "**Yields:**"
260
+ yields_tags.each do |yields_tag|
261
+ yield_text = "- `#{yields_tag.block}`"
262
+ if description = yields_tag.text&.join(" ")
263
+ yield_text << " — #{description}"
264
+ end
265
+ lines << yield_text
266
+ end
267
+ lines << ""
268
+ end
269
+
270
+ # Examples
271
+ examples = documentation.filter(Decode::Comment::Example).to_a
272
+ if examples.any?
273
+ examples.each do |example|
274
+ title = example.title || "Example"
275
+ lines << "**#{title}:**"
276
+ lines << "```ruby"
277
+ lines << example.code if example.code
278
+ lines << "```"
279
+ lines << ""
280
+ end
281
+ end
282
+
283
+ # Description (longer text after summary)
284
+ if description = extract_description(documentation)
285
+ lines << "**Details:**"
286
+ lines << ""
287
+ lines << description
288
+ lines << ""
289
+ end
290
+ end
291
+
292
+ lines
293
+ end
294
+
295
+ # Extract summary (first paragraph) from documentation.
296
+ def extract_summary(documentation)
297
+ return nil unless documentation.text
298
+
299
+ lines = documentation.text
300
+ summary_lines = []
301
+
302
+ lines.each do |line|
303
+ line_str = line.to_s.strip
304
+ break if line_str.empty? && summary_lines.any?
305
+ summary_lines << line_str unless line_str.empty?
306
+ end
307
+
308
+ return nil if summary_lines.empty?
309
+ summary_lines.join(" ")
310
+ end
311
+
312
+ # Extract description (everything after summary) from documentation.
313
+ def extract_description(documentation)
314
+ return nil unless documentation.text
315
+
316
+ lines = documentation.text
317
+ description_lines = []
318
+ found_gap = false
319
+
320
+ lines.each do |line|
321
+ line_str = line.to_s
322
+ if line_str.strip.empty?
323
+ found_gap = true if description_lines.any?
324
+ elsif found_gap
325
+ description_lines << line_str
326
+ end
327
+ end
328
+
329
+ return nil if description_lines.empty?
330
+ description_lines.join("\n")
331
+ end
332
+
333
+ # Generate an overview/index file for the documentation.
334
+ def generate_overview(name, containers, index)
335
+ lines = []
336
+
337
+ lines << "# #{name.capitalize}"
338
+ lines << ""
339
+ lines << "This directory contains documentation for all public classes and modules."
340
+ lines << ""
341
+
342
+ # Group by top-level namespace
343
+ namespaces = {}
344
+ containers.each do |qualified_name, data|
345
+ parts = qualified_name.split("::")
346
+ top_level = parts.first
347
+ namespaces[top_level] ||= []
348
+ namespaces[top_level] << {name: qualified_name, definition: data[:definition]}
349
+ end
350
+
351
+ lines << "## Namespaces"
352
+ lines << ""
353
+
354
+ namespaces.keys.sort.each do |namespace|
355
+ items = namespaces[namespace].sort_by{|item| item[:name]}
356
+
357
+ lines << "### #{namespace}"
358
+ lines << ""
359
+
360
+ items.each do |item|
361
+ definition = item[:definition]
362
+ relative_path = "#{name}/#{item[:name].gsub('::', '/')}.md"
363
+
364
+ if documentation = definition.documentation
365
+ if summary = extract_summary(documentation)
366
+ lines << "- [#{item[:name]}](#{relative_path}) - #{summary}"
367
+ next
368
+ end
369
+ end
370
+
371
+ lines << "- [#{item[:name]}](#{relative_path})"
372
+ end
373
+
374
+ lines << ""
375
+ end
376
+
377
+ lines.join("\n")
378
+ end
@@ -0,0 +1,254 @@
1
+ # Documentation Coverage
2
+
3
+ This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
4
+
5
+ ## Available Bake Tasks
6
+
7
+ The Decode gem provides several bake tasks for analyzing your codebase:
8
+
9
+ - `bake decode:index:coverage` - Check documentation coverage.
10
+ - `bake decode:index:symbols` - List all symbols in the codebase.
11
+ - `bake decode:index:documentation` - Extract all documentation.
12
+
13
+ ## Checking Documentation Coverage
14
+
15
+ ### Basic Coverage Check
16
+
17
+ ```bash
18
+ # Check coverage for the lib directory:
19
+ bake decode:index:coverage lib
20
+
21
+ # Check coverage for a specific directory:
22
+ bake decode:index:coverage app/models
23
+ ```
24
+
25
+ ### Example Output
26
+
27
+ When you run the coverage command, you'll see output like:
28
+
29
+ ```
30
+ Decode
31
+ Decode::VERSION
32
+ Decode::Languages.all
33
+ Decode::Languages#initialize
34
+ Decode::Languages#freeze
35
+ Decode::Languages#add
36
+ Decode::Languages#fetch
37
+ Decode::Languages#source_for
38
+ Decode::Languages::REFERENCE
39
+ Decode::Languages#reference_for
40
+ Decode::Source#initialize
41
+ ... snip ...
42
+ 135/215 definitions have documentation.
43
+ ```
44
+
45
+ Using this tool can show you areas that might require more attention.
46
+
47
+ ### Understanding Coverage Output
48
+
49
+ The coverage check:
50
+ - **Counts only public definitions** (public methods, classes, modules).
51
+ - **Reports the ratio** of documented vs total public definitions.
52
+ - **Lists missing documentation** by qualified name.
53
+ - **Fails with an error** if coverage is incomplete.
54
+
55
+ ### What Counts as "Documented"
56
+
57
+ A definition is considered documented if it has:
58
+ - Any comment preceding it.
59
+ - Documentation pragmas (like `@parameter`, `@returns`, `@example`).
60
+ - A `@namespace` pragma (for organizational modules).
61
+
62
+ ```ruby
63
+ # A user in the system.
64
+ class MyClass
65
+ end
66
+
67
+ # @namespace
68
+ module OrganizationalModule
69
+ # Contains helper functionality.
70
+ end
71
+
72
+ # Process user data and return formatted results.
73
+ # @parameter name [String] The user's name.
74
+ # @returns [bool] Success status.
75
+ def process(name)
76
+ # Validation logic here:
77
+ return false if name.empty?
78
+
79
+ # Processing logic:
80
+ true
81
+ end
82
+
83
+ class UndocumentedClass
84
+ end
85
+ ```
86
+
87
+ ## Analyzing Symbols
88
+
89
+ ### List All Symbols
90
+
91
+ ```bash
92
+ # See the structure of your codebase
93
+ bake decode:index:symbols lib
94
+ ```
95
+
96
+ This shows the hierarchical structure of your code:
97
+
98
+ ```
99
+ [] -> []
100
+ ["MyGem"] -> [#<Decode::Language::Ruby::Module:...>]
101
+ MyGem
102
+ ["MyGem", "User"] -> [#<Decode::Language::Ruby::Class:...>]
103
+ MyGem::User
104
+ ["MyGem", "User", "initialize"] -> [#<Decode::Language::Ruby::Method:...>]
105
+ MyGem::User#initialize
106
+ ```
107
+
108
+ ### Extract Documentation
109
+
110
+ ```bash
111
+ # Extract all documentation from your codebase
112
+ bake decode:index:documentation lib
113
+ ```
114
+
115
+ This outputs formatted documentation for all documented definitions:
116
+
117
+ ~~~markdown
118
+ ## `MyGem::User#initialize`
119
+
120
+ Initialize a new user with the given email address.
121
+
122
+ ## `MyGem::User#authenticate`
123
+
124
+ Authenticate the user with a password.
125
+ Returns true if authentication is successful.
126
+ ~~~
127
+
128
+ ## Achieving 100% Coverage
129
+
130
+ ### Document all public APIs
131
+
132
+ ```ruby
133
+ # A user account with authentication and email.
134
+ class User
135
+ # @attribute [String] The user's email address.
136
+ attr_reader :email
137
+
138
+ # Initialize a new user.
139
+ # @parameter email [String] The user's email address.
140
+ def initialize(email)
141
+ # Store the email address:
142
+ @email = email
143
+ end
144
+ end
145
+ ```
146
+
147
+ ### Use @namespace for organizational modules
148
+
149
+ The best place to add these by default is `version.rb`.
150
+
151
+ ```ruby
152
+ # @namespace
153
+ module MyGem
154
+ VERSION = "0.1.0"
155
+ end
156
+ ```
157
+
158
+ ### Document edge cases
159
+
160
+ ```ruby
161
+ # @private
162
+ def internal_helper
163
+ # Add the fields:
164
+ return foo + bar
165
+ end
166
+ ```
167
+
168
+ ### Common Coverage Issues
169
+
170
+ #### Missing namespace documentation
171
+
172
+ ```ruby
173
+ # This module has no documentation and will show as missing coverage:
174
+ module MyGem
175
+ end
176
+
177
+ # Solution: Add @namespace pragma:
178
+ # @namespace
179
+ module MyGem
180
+ # Provides core functionality.
181
+ end
182
+ ```
183
+
184
+ #### Undocumented methods
185
+
186
+ Problem: Methods without any comments will show as missing coverage:
187
+ ```ruby
188
+ def process_data
189
+ # Implementation here
190
+ end
191
+ ```
192
+
193
+ Solution: Add description and pragmas:
194
+ ```ruby
195
+ # Process the input data and return results.
196
+ # @parameter data [Hash] Input data to process.
197
+ # @returns [Array] Processed results.
198
+ def process_data(data)
199
+ # Process the input:
200
+ results = data.map{|item| transform(item)}
201
+
202
+ # Return processed results:
203
+ results
204
+ end
205
+ ```
206
+
207
+ #### Missing attr documentation
208
+
209
+ Problem: Attributes without documentation will show as missing coverage:
210
+ ```ruby
211
+ attr_reader :name
212
+ ```
213
+
214
+ Solution: Document with @attribute pragma:
215
+ ```ruby
216
+ # @attribute [String] The user's full name.
217
+ attr_reader :name
218
+ ```
219
+
220
+ ## Integrating into CI/CD
221
+
222
+ ### GitHub Actions Example
223
+
224
+ ```yaml
225
+ name: Documentation Coverage
226
+
227
+ on: [push, pull_request]
228
+
229
+ jobs:
230
+ docs:
231
+ runs-on: ubuntu-latest
232
+ steps:
233
+ - uses: actions/checkout@v3
234
+ - uses: ruby/setup-ruby@v1
235
+ with:
236
+ bundler-cache: true
237
+ - name: Check documentation coverage
238
+ run: bake decode:index:coverage lib
239
+ ```
240
+
241
+ ### Rake Task Integration
242
+
243
+ Add to your `Rakefile`:
244
+
245
+ ```ruby
246
+ require "decode"
247
+
248
+ desc "Check documentation coverage"
249
+ task :doc_coverage do
250
+ system("bake decode:index:coverage lib") || exit(1)
251
+ end
252
+
253
+ task default: [:test, :doc_coverage]
254
+ ```