decode 0.26.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: 453b1f28ebb358dd7f37f17d12e6e48616895970d0a9a9225ddb8d3384f2fc90
4
- data.tar.gz: 3720af135885d5d002f91a3bc758a3f04e58c1d7022fc4d90b008754568b66d2
3
+ metadata.gz: 65f2a545a291a4258d019ea79ea9c4cda2e4e15af778670b505ac7652d620ee3
4
+ data.tar.gz: fa7d2328a1eba99917c23de03569575642ae587dbc049ab4a409d84e85824c57
5
5
  SHA512:
6
- metadata.gz: cf74b8ffc88195be9440a81aeceb0da5b2b77ff9c2e45df2961f683b27ed15f1d5ccab54d161f05931d2c9c5fdd5641b50b6122757b91d55d3a5668c8dcbde1a
7
- data.tar.gz: 417e14f2501bdb368a25f5872915d4d3de47cd57f39164977c4a42b372ab41f4a9d4d435644972297f076c84cc3d475f732a4d90677ba0bb7b01e897586ca9b1
6
+ metadata.gz: 78ac2928c18dc95f78aa181cadca912776e288781ef3f88c0fbd507faae38884d2d66dcf73770dafeb874aff4ef69a2ecec601b6b3e097be02154fe7a06f187b
7
+ data.tar.gz: acef5f32fcb5b0ff49c076e706555c6eb2fbceed83abc9bd0f35557e72153fcde17fcd6cc31320eb9aee1c71fd73739554d757517b7105f8d080eee8ad71a0dc
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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
@@ -60,7 +60,7 @@ A definition is considered documented if it has:
60
60
  - A `@namespace` pragma (for organizational modules).
61
61
 
62
62
  ```ruby
63
- # Represents a user in the system.
63
+ # A user in the system.
64
64
  class MyClass
65
65
  end
66
66
 
@@ -130,7 +130,7 @@ Returns true if authentication is successful.
130
130
  ### Document all public APIs
131
131
 
132
132
  ```ruby
133
- # Represents a user management system.
133
+ # A user account with authentication and email.
134
134
  class User
135
135
  # @attribute [String] The user's email address.
136
136
  attr_reader :email
@@ -197,7 +197,7 @@ Solution: Add description and pragmas:
197
197
  # @returns [Array] Processed results.
198
198
  def process_data(data)
199
199
  # Process the input:
200
- results = data.map {|item| transform(item)}
200
+ results = data.map{|item| transform(item)}
201
201
 
202
202
  # Return processed results:
203
203
  results
@@ -9,7 +9,9 @@ This guide covers documentation practices and pragmas supported by the Decode ge
9
9
  #### Definition Documentation
10
10
 
11
11
  - Full sentences: All documentation for definitions (classes, modules, methods) should be written as complete sentences with proper grammar and punctuation.
12
- - Class documentation: Documentation for classes should generally start with "Represents a ..." to clearly indicate what the class models or encapsulates.
12
+ - Class documentation: Should directly describe what the class *is* or *does*.
13
+ - For data/model classes, "A user account in the system." works well.
14
+ - For functional classes (servers, clients, connections), lead with what the class does: "An HTTP client that manages persistent connections...".
13
15
  - Method documentation: Should clearly describe what the method does, not how it does it.
14
16
  - Markdown format: All documentation comments are written in Markdown format, allowing for rich formatting including lists, emphasis, code blocks, and links.
15
17
 
@@ -32,8 +34,12 @@ This guide covers documentation practices and pragmas supported by the Decode ge
32
34
 
33
35
  #### Examples
34
36
 
37
+ ##### Data/Model Classes
38
+
39
+ For classes that model domain concepts, describe what the class is:
40
+
35
41
  ```ruby
36
- # Represents a user account in the system.
42
+ # A user account in the system.
37
43
  class User
38
44
  # @attribute [String] The user's email address.
39
45
  attr_reader :email
@@ -70,26 +76,35 @@ class User
70
76
  true
71
77
  end
72
78
  end
79
+ ```
73
80
 
74
- # Represents a collection of users with search capabilities.
75
- class UserCollection
76
- # Find users matching the given criteria.
77
- # @parameter criteria [Hash] Search parameters.
78
- # @returns [Array(User)] Matching users.
79
- def find(**criteria)
80
- # Start with all users:
81
- results = @users.dup
82
-
83
- # Apply each filter criterion:
84
- criteria.each do |key, value|
85
- results = filter_by(results, key, value)
86
- end
87
-
88
- results
81
+ ##### Functional/Service Classes
82
+
83
+ For classes that *do* something (clients, servers, processors), lead with what the class does:
84
+
85
+ ```ruby
86
+ # An HTTP client that manages persistent connections to a remote server, with automatic retries for idempotent requests.
87
+ class Client
88
+ # Send a request to the remote server.
89
+ # @parameter request [Protocol::HTTP::Request] The request to send.
90
+ # @returns [Protocol::HTTP::Response] The response from the server.
91
+ def call(request)
92
+ # ...
93
+ end
94
+
95
+ # Close the client and release all connections.
96
+ def close
97
+ # ...
89
98
  end
90
99
  end
100
+
101
+ # Raised when a connection to the remote server cannot be established.
102
+ class ConnectionError < StandardError
103
+ end
91
104
  ```
92
105
 
106
+ Note the difference: `User` is described as a thing ("A user account..."), while `Client` is described by what it does ("An HTTP client that manages..."), and `ConnectionError` is described by when it occurs ("Raised when...").
107
+
93
108
  **Key formatting examples from above:**
94
109
  - `{disable!}` - Creates a link to the `disable!` method (relative reference)
95
110
  - `active?` - Formats the method name in monospace (backticks for code formatting)
@@ -155,7 +170,7 @@ Type signatures are used to specify the expected types of parameters, return val
155
170
  Documents class attributes, instance variables, and `attr_*` declarations. Prefer to have one attribute per line for clarity.
156
171
 
157
172
  ```ruby
158
- # Represents a person with basic attributes.
173
+ # A person with basic attributes.
159
174
  class Person
160
175
  # @attribute [String] The person's full name.
161
176
  attr_reader :name
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "location"
7
7
 
@@ -166,6 +166,14 @@ module Decode
166
166
  false
167
167
  end
168
168
 
169
+ # Whether this definition represents an alias to another definition.
170
+ # Tools can use this to filter aliases from outputs without parsing text.
171
+ #
172
+ # @returns [bool] False by default; specific definition types may override.
173
+ def alias?
174
+ false
175
+ end
176
+
169
177
  # Whether this represents a single entity to be documented (along with it's contents).
170
178
  #
171
179
  # @returns [bool]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "definition"
7
7
 
@@ -21,6 +21,18 @@ module Decode
21
21
 
22
22
  attr :old_name
23
23
 
24
+ # Whether this definition represents an alias.
25
+ # @returns [bool] Always true for aliases.
26
+ def alias?
27
+ true
28
+ end
29
+
30
+ # The original name this alias refers to.
31
+ # @returns [Symbol]
32
+ def aliased_name
33
+ @old_name
34
+ end
35
+
24
36
  # Aliases don't require separate documentation as they reference existing methods.
25
37
  # @returns [bool] Always false for aliases.
26
38
  def coverage_relevant?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "definition"
7
7
 
@@ -18,10 +18,23 @@ module Decode
18
18
  super(*arguments, **options)
19
19
 
20
20
  @super_class = super_class
21
+ @includes = []
22
+ @extends = []
23
+ @prepends = []
21
24
  end
22
25
 
26
+ # @attribute [String?] The super class name.
23
27
  attr :super_class
24
28
 
29
+ # @attribute [Array(Symbol)] Modules included into this class.
30
+ attr :includes
31
+
32
+ # @attribute [Array(Symbol)] Modules extended into this class (adds singleton methods).
33
+ attr :extends
34
+
35
+ # @attribute [Array(Symbol)] Modules prepended into this class (method lookup precedence).
36
+ attr :prepends
37
+
25
38
  # A class is a container for other definitions.
26
39
  def container?
27
40
  true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "definition"
7
7
 
@@ -10,6 +10,24 @@ module Decode
10
10
  module Ruby
11
11
  # A Ruby-specific module.
12
12
  class Module < Definition
13
+ # Initialize a module with its name and options.
14
+ def initialize(*arguments, **options)
15
+ super(*arguments, **options)
16
+
17
+ @includes = []
18
+ @extends = []
19
+ @prepends = []
20
+ end
21
+
22
+ # @attribute [Array(Symbol)] Modules included into this module.
23
+ attr :includes
24
+
25
+ # @attribute [Array(Symbol)] Modules extended into this module (adds singleton methods).
26
+ attr :extends
27
+
28
+ # @attribute [Array(Symbol)] Modules prepended into this module (method lookup precedence).
29
+ attr :prepends
30
+
13
31
  # A module is a container for other definitions.
14
32
  def container?
15
33
  true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
 
6
6
  require "prism"
7
7
 
@@ -93,13 +93,13 @@ module Decode
93
93
  path = nested_path_for(node.constant_path)
94
94
 
95
95
  definition = Module.new(path,
96
- visibility: :public,
97
- comments: comments_for(node),
98
- parent: parent,
99
- node: node,
100
- language: @language,
101
- source: source,
102
- )
96
+ visibility: :public,
97
+ comments: comments_for(node),
98
+ parent: parent,
99
+ node: node,
100
+ language: @language,
101
+ source: source,
102
+ )
103
103
 
104
104
  store_definition(parent, path.last.to_sym, definition)
105
105
  yield definition
@@ -114,14 +114,14 @@ module Decode
114
114
  super_class = nested_name_for(node.superclass)
115
115
 
116
116
  definition = Class.new(path,
117
- super_class: super_class,
118
- visibility: :public,
119
- comments: comments_for(node),
120
- parent: parent,
121
- node: node,
122
- language: @language,
123
- source: source,
124
- )
117
+ super_class: super_class,
118
+ visibility: :public,
119
+ comments: comments_for(node),
120
+ parent: parent,
121
+ node: node,
122
+ language: @language,
123
+ source: source,
124
+ )
125
125
 
126
126
  store_definition(parent, path.last.to_sym, definition)
127
127
  yield definition
@@ -134,13 +134,13 @@ module Decode
134
134
  when :singleton_class_node
135
135
  if name = singleton_name_for(node)
136
136
  definition = Singleton.new(name,
137
- comments: comments_for(node),
138
- parent: parent,
139
- node: node,
140
- language: @language,
141
- visibility: :public,
142
- source: source
143
- )
137
+ comments: comments_for(node),
138
+ parent: parent,
139
+ node: node,
140
+ language: @language,
141
+ visibility: :public,
142
+ source: source
143
+ )
144
144
 
145
145
  yield definition
146
146
 
@@ -152,23 +152,23 @@ module Decode
152
152
  receiver = receiver_for(node.receiver)
153
153
 
154
154
  definition = Method.new(node.name,
155
- visibility: @visibility,
156
- comments: comments_for(node),
157
- parent: parent,
158
- node: node,
159
- language: @language,
160
- receiver: receiver,
161
- source: source,
162
- )
155
+ visibility: @visibility,
156
+ comments: comments_for(node),
157
+ parent: parent,
158
+ node: node,
159
+ language: @language,
160
+ receiver: receiver,
161
+ source: source,
162
+ )
163
163
 
164
164
  yield definition
165
165
  when :constant_write_node
166
166
  definition = Constant.new(node.name,
167
- comments: comments_for(node),
168
- parent: parent,
169
- node: node,
170
- language: @language,
171
- )
167
+ comments: comments_for(node),
168
+ parent: parent,
169
+ node: node,
170
+ language: @language,
171
+ )
172
172
 
173
173
  store_definition(parent, node.name, definition)
174
174
  yield definition
@@ -176,6 +176,40 @@ module Decode
176
176
  name = node.name
177
177
 
178
178
  case name
179
+ when :include, :extend, :prepend
180
+ # Handle mixins inside classes/modules
181
+ if parent
182
+ if node.arguments
183
+ node.arguments.arguments.each do |arg|
184
+ mod_name = case arg.type
185
+ when :constant_read_node
186
+ # Qualify with enclosing namespace if available (e.g. Mixins::Greeting)
187
+ if parent.parent && parent.parent.respond_to?(:qualified_name)
188
+ "#{parent.parent.qualified_name}::#{arg.name}"
189
+ else
190
+ arg.name.to_s
191
+ end
192
+ when :constant_path_node
193
+ nested_name_for(arg)
194
+ else
195
+ # Skip unsupported argument types (e.g., dynamic expressions)
196
+ nil
197
+ end
198
+ if mod_name
199
+ case name
200
+ when :include
201
+ parent.respond_to?(:includes) && parent.includes << mod_name
202
+ when :extend
203
+ parent.respond_to?(:extends) && parent.extends << mod_name
204
+ when :prepend
205
+ parent.respond_to?(:prepends) && parent.prepends << mod_name
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ # Don't treat include/extend as definitions.
212
+ return
179
213
  when :public, :protected, :private
180
214
  # Handle cases like "private def foo" where method definitions are arguments
181
215
  if node.arguments
@@ -187,13 +221,13 @@ module Decode
187
221
  receiver = receiver_for(argument_node.receiver)
188
222
 
189
223
  definition = Method.new(argument_node.name,
190
- visibility: name,
191
- comments: comments_for(argument_node),
192
- parent: parent,
193
- node: argument_node,
194
- language: @language,
195
- receiver: receiver,
196
- )
224
+ visibility: name,
225
+ comments: comments_for(argument_node),
226
+ parent: parent,
227
+ node: argument_node,
228
+ language: @language,
229
+ receiver: receiver,
230
+ )
197
231
 
198
232
  yield definition
199
233
  end
@@ -217,9 +251,9 @@ module Decode
217
251
  end
218
252
  when :attr, :attr_reader, :attr_writer, :attr_accessor
219
253
  definition = Attribute.new(attribute_name_for(node),
220
- comments: comments_for(node),
221
- parent: parent, language: @language, node: node
222
- )
254
+ comments: comments_for(node),
255
+ parent: parent, language: @language, node: node
256
+ )
223
257
 
224
258
  yield definition
225
259
  when :alias_method
@@ -233,29 +267,29 @@ module Decode
233
267
  old_name = symbol_name_for(old_name_arg)
234
268
 
235
269
  definition = Alias.new(new_name.to_sym, old_name.to_sym,
236
- comments: comments_for(node),
237
- parent: parent,
238
- node: node,
239
- language: @language,
240
- visibility: @visibility,
241
- source: source,
242
- )
270
+ comments: comments_for(node),
271
+ parent: parent,
272
+ node: node,
273
+ language: @language,
274
+ visibility: @visibility,
275
+ source: source,
276
+ )
243
277
 
244
278
  yield definition
245
279
  end
246
280
  else
247
281
  # Check if this call should be treated as a definition
248
282
  # either because it has a @name comment, @attribute comment, or a block
249
- has_name_comment = comments_for(node).any? {|comment| comment.match(NAME_ATTRIBUTE)}
283
+ has_name_comment = comments_for(node).any?{|comment| comment.match(NAME_ATTRIBUTE)}
250
284
  has_attribute_comment = kind_for(node, comments_for(node))
251
285
  has_block = node.block
252
286
 
253
287
  if has_name_comment || has_attribute_comment || has_block
254
288
  definition = Call.new(
255
- attribute_name_for(node),
256
- comments: comments_for(node),
257
- parent: parent, language: @language, node: node
258
- )
289
+ attribute_name_for(node),
290
+ comments: comments_for(node),
291
+ parent: parent, language: @language, node: node
292
+ )
259
293
 
260
294
  yield definition
261
295
 
@@ -265,19 +299,19 @@ module Decode
265
299
  end
266
300
  end
267
301
  end
268
- when :alias_method_node
269
- # Handle alias new_name old_name syntax
302
+ when :alias_node, :alias_method_node
303
+ # Handle `alias new_name old_name` syntax:
270
304
  new_name = node.new_name.unescaped
271
305
  old_name = node.old_name.unescaped
272
306
 
273
307
  definition = Alias.new(new_name.to_sym, old_name.to_sym,
274
- comments: comments_for(node),
275
- parent: parent,
276
- node: node,
277
- language: @language,
278
- visibility: @visibility,
279
- source: source,
280
- )
308
+ comments: comments_for(node),
309
+ parent: parent,
310
+ node: node,
311
+ language: @language,
312
+ visibility: @visibility,
313
+ source: source,
314
+ )
281
315
 
282
316
  yield definition
283
317
  when :if_node
@@ -550,20 +584,20 @@ module Decode
550
584
  # Start a new segment with these comments
551
585
  yield current_segment if current_segment
552
586
  current_segment = Segment.new(
553
- preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
554
- @language,
555
- statement
556
- )
587
+ preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
588
+ @language,
589
+ statement
590
+ )
557
591
  elsif current_segment
558
592
  # Extend current segment with this statement
559
593
  current_segment.expand(statement)
560
594
  else
561
595
  # Start a new segment without comments
562
596
  current_segment = Segment.new(
563
- [],
564
- @language,
565
- statement
566
- )
597
+ [],
598
+ @language,
599
+ statement
600
+ )
567
601
  end
568
602
  end
569
603
 
@@ -571,10 +605,10 @@ module Decode
571
605
  else
572
606
  # One top level segment:
573
607
  segment = Segment.new(
574
- [],
575
- @language,
576
- node
577
- )
608
+ [],
609
+ @language,
610
+ node
611
+ )
578
612
 
579
613
  yield segment
580
614
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require "rbs"
7
7
  require_relative "wrapper"
@@ -35,6 +35,7 @@ module Decode
35
35
  name: generic.to_sym,
36
36
  variance: nil,
37
37
  upper_bound: nil,
38
+ lower_bound: nil,
38
39
  location: nil
39
40
  )
40
41
  end
@@ -51,10 +52,10 @@ module Decode
51
52
  # Extract super class if present:
52
53
  super_class = if @definition.super_class
53
54
  ::RBS::AST::Declarations::Class::Super.new(
54
- name: qualified_name_to_rbs(@definition.super_class),
55
- args: [],
56
- location: nil
57
- )
55
+ name: qualified_name_to_rbs(@definition.super_class),
56
+ args: [],
57
+ location: nil
58
+ )
58
59
  end
59
60
 
60
61
  # Create the class declaration with generics:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require "rbs"
7
7
  require "console"
@@ -103,15 +103,15 @@ module Decode
103
103
  kind = @definition.receiver ? :singleton : :instance
104
104
 
105
105
  ::RBS::AST::Members::MethodDefinition.new(
106
- name: method_name.to_sym,
107
- kind: kind,
108
- overloads: overloads,
109
- annotations: [],
110
- location: nil,
111
- comment: comment,
112
- overloading: false,
113
- visibility: @definition.visibility || :public
114
- )
106
+ name: method_name.to_sym,
107
+ kind: kind,
108
+ overloads: overloads,
109
+ annotations: [],
110
+ location: nil,
111
+ comment: comment,
112
+ overloading: false,
113
+ visibility: @definition.visibility || :public
114
+ )
115
115
  end
116
116
 
117
117
  # Build a complete RBS function type from AST information.
@@ -265,7 +265,7 @@ module Decode
265
265
 
266
266
  # Find @parameter tags (but not @option tags, which are handled separately):
267
267
  param_tags = documentation.filter(Decode::Comment::Parameter).to_a
268
- param_tags = param_tags.reject {|tag| tag.is_a?(Decode::Comment::Option)}
268
+ param_tags = param_tags.reject{|tag| tag.is_a?(Decode::Comment::Option)}
269
269
  return [] if param_tags.empty?
270
270
 
271
271
  param_tags.map do |tag|
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require "rbs"
7
7
  require_relative "wrapper"
@@ -62,11 +62,11 @@ module Decode
62
62
  type = ::Decode::RBS::Type.parse(type_string)
63
63
 
64
64
  ::RBS::AST::Declarations::Constant.new(
65
- name: constant_definition.name.to_sym,
66
- type: type,
67
- location: nil,
68
- comment: nil
69
- )
65
+ name: constant_definition.name.to_sym,
66
+ type: type,
67
+ location: nil,
68
+ comment: nil
69
+ )
70
70
  end
71
71
  end
72
72
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require "rbs"
7
7
  require "console"
@@ -21,10 +21,10 @@ module Decode
21
21
  true
22
22
  when ::RBS::Types::Union
23
23
  # Type | nil form - recursively check all union members
24
- rbs_type.types.any? {|type| nullable?(type)}
24
+ rbs_type.types.any?{|type| nullable?(type)}
25
25
  when ::RBS::Types::Tuple
26
26
  # [Type] form - recursively check all tuple elements
27
- rbs_type.types.any? {|type| nullable?(type)}
27
+ rbs_type.types.any?{|type| nullable?(type)}
28
28
  when ::RBS::Types::Bases::Nil
29
29
  # Direct nil type
30
30
  true
@@ -5,5 +5,5 @@
5
5
 
6
6
  module Decode
7
7
  # @constant [String] The version of the gem.
8
- VERSION = "0.26.0"
8
+ VERSION = "0.27.0"
9
9
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2020-2025, by Samuel Williams.
3
+ Copyright, 2020-2026, by Samuel Williams.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -24,6 +24,10 @@ Please see the [project documentation](https://socketry.github.io/decode/) for m
24
24
 
25
25
  Please see the [project releases](https://socketry.github.io/decode/releases/index) for all releases.
26
26
 
27
+ ### v0.27.0
28
+
29
+ - Add `decode:documentation:markdown` bake task for generating LLM-optimized Markdown documentation.
30
+
27
31
  ### v0.26.0
28
32
 
29
33
  - Add support for `@example` pragmas in Ruby documentation comments.
@@ -60,6 +64,22 @@ We welcome contributions to this project.
60
64
  4. Push to the branch (`git push origin my-new-feature`).
61
65
  5. Create new Pull Request.
62
66
 
67
+ ### Running Tests
68
+
69
+ To run the test suite:
70
+
71
+ ``` shell
72
+ bundle exec sus
73
+ ```
74
+
75
+ ### Making Releases
76
+
77
+ To make a new release:
78
+
79
+ ``` shell
80
+ bundle exec bake gem:release:patch # or minor or major
81
+ ```
82
+
63
83
  ### Developer Certificate of Origin
64
84
 
65
85
  In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.27.0
4
+
5
+ - Add `decode:documentation:markdown` bake task for generating LLM-optimized Markdown documentation.
6
+
3
7
  ## v0.26.0
4
8
 
5
9
  - Add support for `@example` pragmas in Ruby documentation comments.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -71,6 +71,7 @@ extensions: []
71
71
  extra_rdoc_files: []
72
72
  files:
73
73
  - agent.md
74
+ - bake/decode/documentation.rb
74
75
  - bake/decode/index.rb
75
76
  - bake/decode/rbs.rb
76
77
  - context/documentation-coverage.md
@@ -150,14 +151,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
150
151
  requirements:
151
152
  - - ">="
152
153
  - !ruby/object:Gem::Version
153
- version: '3.2'
154
+ version: '3.3'
154
155
  required_rubygems_version: !ruby/object:Gem::Requirement
155
156
  requirements:
156
157
  - - ">="
157
158
  - !ruby/object:Gem::Version
158
159
  version: '0'
159
160
  requirements: []
160
- rubygems_version: 3.7.2
161
+ rubygems_version: 4.0.6
161
162
  specification_version: 4
162
163
  summary: Code analysis for documentation generation.
163
164
  test_files: []
metadata.gz.sig CHANGED
Binary file