agentic examples

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

source on github

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