The API Surface Census
The API Surface Census: your public API is not what you documented - it's every public method a user CAN call, because that's what semver binds you to. This census counts the whole surface, then cross-references 100 example programs to split it into the API people actually use and the accidental API nobody asked for but everyone can break themselves against.
Data & Pipelines
Round 12
Yehuda Katz (wycats)
exit 0
bundle exec ruby examples/api_surface.rb
a real captured run
API SURFACE CENSUS (11 core classes vs 177 example programs) class surface exercised accidental (public, unused by any example) PlanOrchestrator 19 10 find_eligible_tasks, overall_status, retry? Task 26 16 agent_spec, retry_count=, output_schema ExecutionJournal 6 5 fsync_every RateLimit 8 7 in_flight CapabilitySpecification 12 10 requirements_description, from_h CapabilityValidator 2 2 CapabilityProvider 3 2 implementation TaskFailure 11 6 context, possibly_transient?, timestamp TaskResult 7 5 task_id, failed? PlanExecutionResult 16 7 in_progress?, plan_id, completed_tasks_count RelationRules 4 2 present?, validate_declaration! total public surface: 114 methods; 72 (63%) exercised by the corpus. reading the census like a steward: the exercised set is your REAL API - 100 programs voted with their call sites, and every one of those methods now carries a semver promise whether the docs say so or not. the accidental set (42 methods) is surface you're paying interest on without collecting rent: each is a thing a user could couple to tomorrow, constraining refactors forever. the move isn't deletion - it's DECLARATION: mark them @api private (or make them private) while nobody depends on them, because the day after somebody does, they're yours for a major version. public-by-default is a loan; the census is the bill.
source
# frozen_string_literal: true # The API Surface Census: your public API is not what you documented - # it's every public method a user CAN call, because that's what semver # binds you to. This census counts the whole surface, then # cross-references 100 example programs to split it into the API # people actually use and the accidental API nobody asked for but # everyone can break themselves against. # # bundle exec ruby examples/api_surface.rb # # Runs offline; the examples directory is the usage corpus. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal # Load everything so the census sees the whole surface Zeitwerk:class="y">:Registry.loaders.each(&class="y">:eager_load) if defined?(Zeitwerk:class="y">:Registry) CORE = [ Agentic:class="y">:PlanOrchestrator, Agentic:class="y">:Task, Agentic:class="y">:ExecutionJournal, Agentic:class="y">:RateLimit, Agentic:class="y">:CapabilitySpecification, Agentic:class="y">:CapabilityValidator, Agentic:class="y">:CapabilityProvider, Agentic:class="y">:TaskFailure, Agentic:class="y">:TaskResult, Agentic:class="y">:PlanExecutionResult, Agentic:class="y">:RelationRules ].freeze corpus = Dir[File.join(__dir__, class="s">"*.rb")].reject { |f| f.end_with?(class="s">"api_surface.rb") } .map { |f| File.read(f, encoding: class="s">"UTF-8") }.join(class="s">"\n") puts class="s">"API SURFACE CENSUS (#{CORE.size} core classes vs #{Dir[File.join(__dir__, "*.rbclass="s">")].size - 1} example programs)" puts puts format(class="s">" %-26s %-9s %-11s %s", class="s">"class", class="s">"surface", class="s">"exercised", class="s">"accidental (public, unused by any example)") total_surface = 0 total_exercised = 0 accidental_all = [] CORE.each do |klass| # Owner-checked: only methods this class itself defines count as ITS # surface - inherited Object/Psych noise is someone else's ledger methods = (klass.public_instance_methods(false) + klass.singleton_class.public_instance_methods(false).select { |m| klass.singleton_class.instance_method(m).owner == klass.singleton_class }).uniq.reject { |m| m.to_s.start_with?(class="s">"_") } used, unused = methods.partition { |m| corpus.match?(/\.#{Regexp.escape(m.to_s.chomp(class="s">"?").chomp(class="s">"!"))}\b/) || corpus.include?(class="s">".#{m}") } total_surface += methods.size total_exercised += used.size accidental_all.concat(unused.map { |m| class="s">"#{klass.name.split("::class="s">").last}##{m}" }) puts format(class="s">" %-26s %-9d %-11d %s", klass.name.split(class="s">"::").last, methods.size, used.size, unused.take(3).join(class="s">", ")) end puts puts format(class="s">" total public surface: %d methods; %d (%.0f%%) exercised by the corpus.", total_surface, total_exercised, total_exercised * 100.0 / total_surface) puts puts class="s">" reading the census like a steward: the exercised set is your REAL" puts class="s">" API - 100 programs voted with their call sites, and every one of" puts class="s">" those methods now carries a semver promise whether the docs say" puts class="s">" so or not. the accidental set (#{accidental_all.size} methods) is surface you're" puts class="s">" paying interest on without collecting rent: each is a thing a" puts class="s">" user could couple to tomorrow, constraining refactors forever." puts class="s">" the move isn't deletion - it's DECLARATION: mark them @api" puts class="s">" private (or make them private) while nobody depends on them," puts class="s">" because the day after somebody does, they're yours for a major" puts class="s">" version. public-by-default is a loan; the census is the bill."