chiridion 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -0
  5. data/lib/chiridion/config.rb +128 -0
  6. data/lib/chiridion/engine/class_linker.rb +204 -0
  7. data/lib/chiridion/engine/document_model.rb +299 -0
  8. data/lib/chiridion/engine/drift_checker.rb +146 -0
  9. data/lib/chiridion/engine/extractor.rb +311 -0
  10. data/lib/chiridion/engine/file_renderer.rb +717 -0
  11. data/lib/chiridion/engine/file_writer.rb +160 -0
  12. data/lib/chiridion/engine/frontmatter_builder.rb +248 -0
  13. data/lib/chiridion/engine/generated_rbs_loader.rb +344 -0
  14. data/lib/chiridion/engine/github_linker.rb +87 -0
  15. data/lib/chiridion/engine/inline_rbs_loader.rb +207 -0
  16. data/lib/chiridion/engine/post_processor.rb +86 -0
  17. data/lib/chiridion/engine/rbs_loader.rb +150 -0
  18. data/lib/chiridion/engine/rbs_type_alias_loader.rb +116 -0
  19. data/lib/chiridion/engine/renderer.rb +598 -0
  20. data/lib/chiridion/engine/semantic_extractor.rb +740 -0
  21. data/lib/chiridion/engine/semantic_renderer.rb +334 -0
  22. data/lib/chiridion/engine/spec_example_loader.rb +84 -0
  23. data/lib/chiridion/engine/template_renderer.rb +275 -0
  24. data/lib/chiridion/engine/type_merger.rb +126 -0
  25. data/lib/chiridion/engine/writer.rb +134 -0
  26. data/lib/chiridion/engine.rb +359 -0
  27. data/lib/chiridion/semantic_engine.rb +186 -0
  28. data/lib/chiridion/version.rb +5 -0
  29. data/lib/chiridion.rb +106 -0
  30. data/templates/constants.liquid +27 -0
  31. data/templates/document.liquid +48 -0
  32. data/templates/file.liquid +108 -0
  33. data/templates/index.liquid +21 -0
  34. data/templates/method.liquid +43 -0
  35. data/templates/methods.liquid +11 -0
  36. data/templates/type_aliases.liquid +26 -0
  37. data/templates/types.liquid +11 -0
  38. metadata +146 -0
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "logger"
5
+
6
+ module Chiridion
7
+ # Semantic documentation engine - outputs structured JSON data.
8
+ #
9
+ # This is an alternative to the regular Engine that focuses on semantic
10
+ # extraction and outputs machine-readable JSON alongside markdown. It's
11
+ # useful for:
12
+ #
13
+ # - Verifying what data is being captured
14
+ # - Debugging the extraction pipeline
15
+ # - Generating LLM-friendly documentation
16
+ # - Separating extraction from presentation
17
+ #
18
+ # Usage:
19
+ # engine = Chiridion::SemanticEngine.new(
20
+ # paths: ['lib/myproject'],
21
+ # output: 'docs/sys',
22
+ # namespace_filter: 'MyProject::'
23
+ # )
24
+ # engine.refresh
25
+ #
26
+ class SemanticEngine
27
+ attr_reader :paths, :output
28
+
29
+ def initialize(
30
+ paths:,
31
+ output:,
32
+ namespace_filter: nil,
33
+ namespace_strip: nil,
34
+ include_specs: false,
35
+ verbose: false,
36
+ logger: nil,
37
+ root: Dir.pwd,
38
+ rbs_path: "sig",
39
+ spec_path: "test",
40
+ project_title: "API Documentation",
41
+ project_description: nil
42
+ )
43
+ @paths = Array(paths)
44
+ @output = output
45
+ @namespace_filter = namespace_filter
46
+ @namespace_strip = namespace_strip || namespace_filter
47
+ @include_specs = include_specs
48
+ @verbose = verbose
49
+ @logger = logger || DefaultLogger.new
50
+ @root = root
51
+ @rbs_path = rbs_path
52
+ @spec_path = spec_path
53
+ @project_title = project_title
54
+ @project_description = project_description
55
+ end
56
+
57
+ def refresh
58
+ require "yard"
59
+ register_rbs_tag
60
+
61
+ @logger.info "Semantic extraction from #{paths_description}..."
62
+
63
+ # Load sources
64
+ load_sources
65
+
66
+ # Extract using SemanticExtractor
67
+ project_doc = extract_documentation
68
+
69
+ # Render to JSON+markdown
70
+ files = render_documentation(project_doc)
71
+
72
+ # Write files
73
+ write_files(files)
74
+
75
+ @logger.info "Semantic docs written to #{@output}/ (#{files.size} files)"
76
+ end
77
+
78
+ private
79
+
80
+ def paths_description = @paths.size == 1 ? @paths.first : "#{@paths.size} paths"
81
+
82
+ def register_rbs_tag
83
+ return if YARD::Tags::Library.labels.key?(:rbs)
84
+
85
+ YARD::Tags::Library.define_tag("RBS type annotation", :rbs, :with_types_and_name)
86
+ end
87
+
88
+ def load_sources
89
+ YARD::Logger.instance.level = @verbose ? ::Logger::WARN : ::Logger::ERROR
90
+
91
+ @source_files = @paths.flat_map { |p| resolve_ruby_files(p) }.map { |f| File.expand_path(f) }
92
+
93
+ YARD::Registry.clear
94
+ YARD.parse(@source_files)
95
+
96
+ # Load from generated RBS (authoritative types)
97
+ rbs_generated_dir = find_rbs_generated_dir
98
+ if rbs_generated_dir
99
+ @rbs_data = Engine::GeneratedRbsLoader.new(verbose: @verbose, logger: @logger).load(rbs_generated_dir)
100
+ @logger.info "Loaded types from #{rbs_generated_dir}" if @verbose
101
+ else
102
+ @rbs_data = Engine::GeneratedRbsLoader::Result.new(
103
+ signatures: {}, ivars: {}, attrs: {}, type_aliases: {}, overloads: {}
104
+ )
105
+ end
106
+
107
+ # Load spec examples if enabled
108
+ @spec_examples = @include_specs ? Engine::SpecExampleLoader.new(@spec_path, @verbose, @logger).load : {}
109
+ end
110
+
111
+ def find_rbs_generated_dir
112
+ generated_dir = File.join(@root, @rbs_path, "generated")
113
+ return generated_dir if Dir.exist?(generated_dir)
114
+
115
+ sig_dir = File.join(@root, @rbs_path)
116
+ return sig_dir if Dir.exist?(sig_dir)
117
+
118
+ nil
119
+ end
120
+
121
+ def resolve_ruby_files(path)
122
+ if File.directory?(path)
123
+ Dir.glob("#{path}/**/*.rb")
124
+ elsif File.file?(path) && path.end_with?(".rb")
125
+ [path]
126
+ else
127
+ @logger.warn "Skipping invalid path: #{path}"
128
+ []
129
+ end
130
+ end
131
+
132
+ def extract_documentation
133
+ extractor = Engine::SemanticExtractor.new(
134
+ rbs_types: @rbs_data.signatures,
135
+ rbs_attr_types: @rbs_data.attrs,
136
+ rbs_ivar_types: @rbs_data.ivars,
137
+ type_aliases: @rbs_data.type_aliases,
138
+ spec_examples: @spec_examples,
139
+ namespace_filter: @namespace_filter,
140
+ logger: @logger
141
+ )
142
+
143
+ extractor.extract(
144
+ YARD::Registry,
145
+ title: @project_title,
146
+ description: @project_description
147
+ )
148
+ end
149
+
150
+ def render_documentation(project_doc)
151
+ renderer = Engine::SemanticRenderer.new(
152
+ namespace_strip: @namespace_strip,
153
+ project_title: @project_title
154
+ )
155
+
156
+ renderer.render(project_doc)
157
+ end
158
+
159
+ def write_files(files)
160
+ FileUtils.mkdir_p(@output)
161
+
162
+ files.each do |filename, content|
163
+ filepath = File.join(@output, filename)
164
+ dir = File.dirname(filepath)
165
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
166
+
167
+ # Only write if content changed
168
+ if File.exist?(filepath)
169
+ existing = File.read(filepath)
170
+ next if existing == content
171
+ end
172
+
173
+ File.write(filepath, content)
174
+ @logger.info " Wrote #{filename}" if @verbose
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # Load required components
181
+ require_relative "engine/document_model"
182
+ require_relative "engine/generated_rbs_loader"
183
+ require_relative "engine/semantic_extractor"
184
+ require_relative "engine/semantic_renderer"
185
+ require_relative "engine/type_merger"
186
+ require_relative "engine/spec_example_loader"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chiridion
4
+ VERSION = "0.3.4"
5
+ end
data/lib/chiridion.rb ADDED
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chiridion/version"
4
+ require_relative "chiridion/config"
5
+ require_relative "chiridion/engine"
6
+
7
+ # Chiridion: Agent-oriented documentation generator for Ruby projects.
8
+ #
9
+ # Chiridion ("handbook" in Greek, from the same root as Enchiridion) generates
10
+ # documentation optimized for AI agents and LLMs working with Ruby codebases.
11
+ # It extracts documentation from YARD comments, merges RBS type signatures,
12
+ # and produces structured markdown suitable for context injection.
13
+ #
14
+ # ## Key Features
15
+ #
16
+ # - **YARD Integration**: Extracts docstrings, @param, @return, @example tags
17
+ # - **RBS Authority**: RBS type signatures are authoritative over YARD types
18
+ # - **Spec Examples**: Extracts usage examples from RSpec files
19
+ # - **Wikilinks**: Obsidian-compatible [[Class::Name]] cross-references
20
+ # - **Drift Detection**: CI mode to ensure docs stay in sync with code
21
+ #
22
+ # ## Design Philosophy
23
+ #
24
+ # Traditional documentation is written for human developers reading in browsers.
25
+ # Agent-oriented documentation is optimized for LLMs processing in context windows:
26
+ #
27
+ # - Structured frontmatter with navigation metadata
28
+ # - Explicit type information (not just prose descriptions)
29
+ # - Cross-reference links that can be followed programmatically
30
+ # - Compact but complete method signatures
31
+ #
32
+ # @example Basic usage
33
+ # engine = Chiridion::Engine.new(
34
+ # paths: ['lib/myproject'],
35
+ # output: 'docs/sys',
36
+ # namespace_filter: 'MyProject::'
37
+ # )
38
+ # engine.refresh
39
+ #
40
+ # @example Configuration block
41
+ # Chiridion.configure do |config|
42
+ # config.output = 'docs/sys'
43
+ # config.namespace_filter = 'MyProject::'
44
+ # config.github_repo = 'user/repo'
45
+ # config.include_specs = true
46
+ # end
47
+ # Chiridion.refresh(['lib/myproject'])
48
+ #
49
+ module Chiridion
50
+ class Error < StandardError; end
51
+
52
+ class << self
53
+ # @return [Config] Global configuration instance
54
+ def config = @config ||= Config.new
55
+
56
+ # Configure Chiridion with a block.
57
+ #
58
+ # @yield [Config] Configuration object
59
+ # @return [Config] Configured instance
60
+ def configure
61
+ yield config
62
+ config
63
+ end
64
+
65
+ # Reset configuration to defaults (useful for testing).
66
+ def reset_config! = @config = Config.new
67
+
68
+ # Convenience method to run documentation refresh.
69
+ #
70
+ # @param paths [Array<String>] Source paths to document
71
+ # @param output [String, nil] Override output directory
72
+ # @return [void]
73
+ def refresh(paths = nil, output: nil)
74
+ engine = Engine.new(
75
+ paths: paths || [config.source_path],
76
+ output: output || config.output,
77
+ namespace_filter: config.namespace_filter,
78
+ include_specs: config.include_specs,
79
+ verbose: config.verbose,
80
+ logger: config.logger,
81
+ inline_source_threshold: config.inline_source_threshold,
82
+ output_mode: config.output_mode
83
+ )
84
+ engine.refresh
85
+ end
86
+
87
+ # Convenience method to check for documentation drift.
88
+ #
89
+ # @param paths [Array<String>] Source paths to check
90
+ # @return [void]
91
+ # @raise [SystemExit] Exits with code 1 if drift detected
92
+ def check(paths = nil)
93
+ engine = Engine.new(
94
+ paths: paths || [config.source_path],
95
+ output: config.output,
96
+ namespace_filter: config.namespace_filter,
97
+ include_specs: config.include_specs,
98
+ verbose: config.verbose,
99
+ logger: config.logger,
100
+ inline_source_threshold: config.inline_source_threshold,
101
+ output_mode: config.output_mode
102
+ )
103
+ engine.check
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,27 @@
1
+ {% comment %}
2
+ Constants section template.
3
+ Variables:
4
+ - constants: Array of {name:, value:, docstring:, is_complex:}
5
+ - complex_constants: Array of complex constants for separate rendering
6
+ {% endcomment %}
7
+ ## Constants
8
+ {%- assign has_simple = false %}{% for c in constants %}{% unless c.is_complex %}{% assign has_simple = true %}{% endunless %}{% endfor %}
9
+ {%- if has_simple %}
10
+
11
+ | Constant | Value | Description |
12
+ |----------|-------|-------------|
13
+ {% for c in constants %}{%- unless c.is_complex -%}
14
+ | `{{ c.name }}` | `{{ c.value | escape_pipes }}` | {{ c.docstring | strip_rbs_blocks | strip_newlines }} |
15
+ {% endunless -%}{% endfor -%}
16
+ {%- endif -%}
17
+ {%- for c in complex_constants %}
18
+
19
+ ### {{ c.name }}
20
+ {% assign clean_doc = c.docstring | strip_rbs_blocks | normalize_headers: 4 -%}
21
+ {%- if clean_doc != blank %}
22
+ {{ clean_doc }}
23
+ {% endif -%}
24
+ ```ruby
25
+ {{ c.name }} = {{ c.value }}
26
+ ```
27
+ {%- endfor -%}
@@ -0,0 +1,48 @@
1
+ {%- comment -%}Class/module document template.{%- endcomment -%}
2
+ # {{ title }}
3
+
4
+ {{ docstring | normalize_headers: 3 }}
5
+ {%- if mixins %}
6
+
7
+ {{ mixins }}
8
+ {%- endif -%}
9
+ {%- if examples.size > 0 %}
10
+
11
+ ## Example
12
+ {%- for ex in examples %}
13
+ {%- unless ex.name == blank or ex.name == '' %}
14
+
15
+ **{{ ex.name }}**
16
+ {%- endunless %}
17
+
18
+ ```ruby
19
+ {{ ex.text }}
20
+ ```
21
+ {%- unless forloop.last %}
22
+ {% endunless -%}
23
+ {%- endfor -%}
24
+ {%- endif -%}
25
+ {%- if spec_examples %}
26
+
27
+ {{ spec_examples }}
28
+ {%- endif -%}
29
+ {%- if see_also %}
30
+
31
+ {{ see_also }}
32
+ {%- endif -%}
33
+ {%- if constants_section != blank %}
34
+
35
+ {{ constants_section }}
36
+ {%- endif -%}
37
+ {%- if types_section != blank %}
38
+
39
+ {{ types_section }}
40
+ {%- endif -%}
41
+ {%- if attributes_section != blank %}
42
+
43
+ {{ attributes_section }}
44
+ {%- endif -%}
45
+ {%- if methods_section != blank %}
46
+
47
+ {{ methods_section }}
48
+ {%- endif -%}
@@ -0,0 +1,108 @@
1
+ {% comment %}
2
+ Per-file documentation template.
3
+
4
+ Variables:
5
+ - path: Source file path (relative)
6
+ - filename: Just the filename
7
+ - line_count: Total lines in source
8
+ - namespaces: Array of namespace objects, each with:
9
+ - name: Short name
10
+ - path: Full path (e.g., "Archema::Query")
11
+ - type: "class" or "module"
12
+ - superclass: Parent class path (if class)
13
+ - docstring: Main documentation
14
+ - mixins: Pre-rendered mixin line
15
+ - examples: Array of {name:, code:}
16
+ - notes: Array of note strings
17
+ - type_aliases: Array of {name:, definition:, description:}
18
+ - types_section: Pre-rendered referenced types (## Types Used)
19
+ - constants_section: Pre-rendered constants
20
+ - summary_section: Pre-rendered attributes + method signatures
21
+ - methods_section: Pre-rendered methods
22
+ - private_summary: Pre-rendered private methods list
23
+ {% endcomment %}
24
+ {% for ns in namespaces %}
25
+ {%- unless forloop.first %}
26
+
27
+ ---
28
+ {% endunless %}
29
+ # {{ ns.type | capitalize }}: {{ ns.path }}
30
+ {%- if ns.superclass %}
31
+
32
+ **Extends:** {{ ns.superclass }}
33
+ {%- endif %}
34
+ {%- if ns.mixins %}
35
+
36
+ {{ ns.mixins }}
37
+ {%- endif %}
38
+ {%- if ns.abstract %}
39
+
40
+ > **Abstract:** This {{ ns.type }} must be subclassed.
41
+ {%- endif %}
42
+ {%- if ns.deprecated %}
43
+
44
+ > **Deprecated:** {{ ns.deprecated }}
45
+ {%- endif %}
46
+ {%- if ns.docstring != blank %}
47
+
48
+ {{ ns.docstring | normalize_headers: 3 }}
49
+ {%- endif %}
50
+ {%- if ns.notes.size > 0 %}
51
+ {% for note in ns.notes %}
52
+
53
+ > **Note:** {{ note }}
54
+ {%- endfor %}
55
+ {%- endif %}
56
+ {%- if ns.examples.size > 0 %}
57
+
58
+ ### Example
59
+ {%- for ex in ns.examples %}
60
+ {%- if ex.name != blank %}
61
+
62
+ **{{ ex.name }}**
63
+ {%- endif %}
64
+
65
+ ```ruby
66
+ {{ ex.code }}
67
+ ```
68
+ {%- endfor %}
69
+ {%- endif %}
70
+ {%- if ns.see_also.size > 0 %}
71
+
72
+ **See also:** {% for see in ns.see_also %}{{ see.target }}{% if see.text %} ({{ see.text }}){% endif %}{% unless forloop.last %}, {% endunless %}{% endfor %}
73
+ {%- endif %}
74
+ {%- if ns.type_aliases.size > 0 %}
75
+
76
+ ## Types
77
+ {%- for ta in ns.type_aliases %}
78
+
79
+ ### {{ ta.name }}
80
+
81
+ : `{{ ta.definition }}`
82
+ {%- if ta.description != blank %}
83
+
84
+ {{ ta.description }}
85
+ {%- endif %}
86
+ {%- endfor %}
87
+ {%- endif %}
88
+ {%- if ns.types_section != blank %}
89
+
90
+ {{ ns.types_section }}
91
+ {%- endif %}
92
+ {%- if ns.constants_section != blank %}
93
+
94
+ {{ ns.constants_section }}
95
+ {%- endif %}
96
+ {%- if ns.summary_section != blank %}
97
+
98
+ {{ ns.summary_section }}
99
+ {%- endif %}
100
+ {%- if ns.methods_section != blank %}
101
+
102
+ {{ ns.methods_section }}
103
+ {%- endif %}
104
+ {%- if ns.private_summary != blank %}
105
+
106
+ {{ ns.private_summary }}
107
+ {%- endif %}
108
+ {% endfor -%}
@@ -0,0 +1,21 @@
1
+ {% comment %}
2
+ Index page template for documentation.
3
+ Variables:
4
+ - title: Project title
5
+ - description: Index page description
6
+ - classes: Array of {path:, link_path:}
7
+ - modules: Array of {path:, link_path:}
8
+ {% endcomment %}
9
+ # {{ title }}
10
+
11
+ > {{ description }}
12
+
13
+ ## Classes
14
+
15
+ {% for klass in classes %}- [[{{ klass.link_path }}|{{ klass.path }}]]
16
+ {% endfor %}
17
+
18
+ ## Modules
19
+
20
+ {% for mod in modules %}- [[{{ mod.link_path }}|{{ mod.path }}]]
21
+ {% endfor %}
@@ -0,0 +1,43 @@
1
+ {%- comment -%}Method documentation template.{%- endcomment -%}
2
+ ### {{ display_name }}{% if has_params %}(...){% endif %}
3
+ {%- if docstring %}
4
+ {{ docstring | normalize_headers: 4 }}
5
+ {%- endif -%}
6
+ {%- if params.size > 0 or return_line %}
7
+
8
+ {% for param in params -%}
9
+ {{ param }}
10
+ {% endfor -%}
11
+ {%- if return_line -%}
12
+ {{ return_line }}
13
+ {% endif %}
14
+ {%- endif -%}
15
+ {%- for ex in examples %}
16
+
17
+ **{% if ex.name %}Example: {{ ex.name }}{% else %}Example{% endif %}:**
18
+
19
+ ```ruby
20
+ {{ ex.text }}
21
+ ```
22
+ {%- endfor -%}
23
+ {%- if behaviors.size > 0 %}
24
+
25
+ **Tested behaviors:**
26
+ {%- for b in behaviors %}
27
+ - {{ b }}
28
+ {%- endfor -%}
29
+ {%- endif -%}
30
+ {%- for ex in spec_examples %}
31
+
32
+ **From specs ({{ ex.name }}):**
33
+
34
+ ```ruby
35
+ {{ ex.code }}
36
+ ```
37
+ {%- endfor -%}
38
+ {%- if inline_source %}
39
+
40
+ ```ruby
41
+ {{ inline_source }}
42
+ ```
43
+ {%- endif -%}
@@ -0,0 +1,11 @@
1
+ {%- comment -%}Methods section template. --- is an overline for each method after the first.{%- endcomment -%}
2
+ ## Methods
3
+
4
+ {% for method in methods -%}
5
+ {%- unless forloop.first %}
6
+
7
+
8
+ ---
9
+ {% endunless -%}
10
+ {{ method }}
11
+ {%- endfor -%}
@@ -0,0 +1,26 @@
1
+ {% comment %}
2
+ Type aliases reference page template.
3
+ Variables:
4
+ - title: Page title
5
+ - description: Page description
6
+ - namespaces: Array of { name:, types: [...] } where types have name, definition, description
7
+ {% endcomment %}
8
+ # {{ title }}
9
+
10
+ > {{ description }}
11
+
12
+ {% for ns in namespaces %}
13
+ ## {{ ns.name }}
14
+
15
+ {% for type in ns.types %}
16
+ ### `{{ type.name }}`
17
+
18
+ {% if type.description %}{{ type.description }}
19
+
20
+ {% endif %}
21
+ ```rbs
22
+ type {{ type.name }} = {{ type.definition }}
23
+ ```
24
+
25
+ {% endfor %}
26
+ {% endfor %}
@@ -0,0 +1,11 @@
1
+ {%- comment -%}Types section template - displays type aliases used by a class/module.{%- endcomment -%}
2
+ ## Types
3
+
4
+ {%- for type in types %}
5
+
6
+ ---
7
+ ### {{ type.name }}
8
+ {% if type.description %}(*{{ type.description }}*)
9
+ {% endif %}
10
+ `{{ type.definition }}`
11
+ {%- endfor -%}