The Documentation Surveyor
The Documentation Surveyor: measures YARD comment coverage for every public method in a lib/ tree. One survey task per file fans out; a single report task fans all the surveys in through the dependency pipe and renders the coverage table.
Data & Pipelines
Round 3
Xavier Noria
exit 0
bundle exec ruby examples/doc_coverage.rb
a real captured run
DOCUMENTATION SURVEY of /opt/hostedtoolcache/Ruby/3.3.11/x64/lib/ruby/gems/3.3.0/bundler/gems/agentic-0fe3f91c1ef9/lib
357/394 public methods documented (90.6%)
least documented files:
0.0% agentic/cli/config.rb (0/4)
missing: #list (line 15)
missing: #get (line 33)
0.0% agentic/cli/agent.rb (0/3)
missing: #list (line 8)
missing: #create (line 17)
0.0% agentic/cli/capabilities.rb (0/3)
missing: #list (line 12)
missing: #show (line 82)
7.1% agentic/cli.rb (1/14)
missing: #exit_on_failure? (line 14)
missing: #version (line 25)
33.3% agentic/factory_methods.rb (1/3)
missing: #included (line 5)
missing: #build (line 20)
source
# frozen_string_literal: true # The Documentation Surveyor: measures YARD comment coverage for every # public method in a lib/ tree. One survey task per file fans out; a # single report task fans all the surveys in through the dependency # pipe and renders the coverage table. # # bundle exec ruby examples/doc_coverage.rb [lib_dir] # # Runs offline; Prism reads the definitions, the comments speak for # themselves. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"prism" LIB = ARGV.first ? File.expand_path(ARGV.first) : File.join(Gem:class="y">:Specification.find_by_name(class="s">"agentic").gem_dir, class="s">"lib") # the installed gem, wherever bundler put it # Walks a parse tree counting public defs and whether a comment # immediately precedes each one def survey(parsed) comment_lines = parsed.comments.map { |c| c.location.start_line }.to_set stats = {documented: 0, undocumented: [], private_from: nil} # Track `private` markers statement-by-statement within class bodies walk = lambda do |node, private_scope| return unless node if node.is_a?(Prism:class="y">:ClassNode) || node.is_a?(Prism:class="y">:ModuleNode) inner = false node.child_nodes.each { |child| inner = walk.call(child, inner) || inner } private_scope elsif node.is_a?(Prism:class="y">:StatementsNode) scope = private_scope node.child_nodes.each { |child| scope = walk.call(child, scope) || scope } scope elsif node.is_a?(Prism:class="y">:CallNode) && node.name == class="y">:private && node.receiver.nil? && node.arguments.nil? true elsif node.is_a?(Prism:class="y">:DefNode) unless private_scope if comment_lines.include?(node.location.start_line - 1) stats[class="y">:documented] += 1 else stats[class="y">:undocumented] << {name: node.name.to_s, line: node.location.start_line} end end false else node.child_nodes.each { |child| walk.call(child, private_scope) } false end end walk.call(parsed.value, false) stats end orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 8) files = Dir[File.join(LIB, class="s">"**", class="s">"*.rb")].sort if files.empty? puts class="s">" no ruby files under #{LIB} - an empty survey proves nothing" exit 1 end surveys = files.map do |path| task = Agentic:class="y">:Task.new( description: path.delete_prefix(class="s">"#{LIB}/"), agent_spec: {class="s">"name" => class="s">"Surveyor", class="s">"instructions" => class="s">"Survey documentation"}, payload: path ) orchestrator.add_task(task, agent: ->(t) { survey(Prism.parse_file(t.payload)) }) task end report = Agentic:class="y">:Task.new( description: class="s">"coverage report", agent_spec: {class="s">"name" => class="s">"Reporter", class="s">"instructions" => class="s">"Aggregate"} ) orchestrator.add_task(report, surveys, agent: ->(t) { rows = surveys.map { |s| stats = t.output_of(s) total = stats[class="y">:documented] + stats[class="y">:undocumented].size {file: s.description, documented: stats[class="y">:documented], total: total, missing: stats[class="y">:undocumented]} } covered = rows.sum { |r| r[class="y">:documented] } total = rows.sum { |r| r[class="y">:total] } {rows: rows, covered: covered, total: total} }) result = orchestrator.execute_plan data = result.results[report.id].output puts class="s">"DOCUMENTATION SURVEY of #{LIB}" puts format(class="s">" %d/%d public methods documented (%.1f%%)", data[class="y">:covered], data[class="y">:total], 100.0 * data[class="y">:covered] / data[class="y">:total]) puts worst = data[class="y">:rows].select { |r| r[class="y">:total] > 0 } .sort_by { |r| [Float(r[class="y">:documented]) / r[class="y">:total], -r[class="y">:total]] }.first(5) puts class="s">"least documented files:" worst.each do |row| puts format(class="s">" %5.1f%% %-46s (%d/%d)", 100.0 * row[class="y">:documented] / row[class="y">:total], row[class="y">:file], row[class="y">:documented], row[class="y">:total]) row[class="y">:missing].first(2).each { |m| puts class="s">" missing: ##{m[class="y">:name]} (line #{m[class="y">:line]})" } end